1 /* 2 * Copyright (C) 2011 The Libphonenumber Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.i18n.phonenumbers; 18 19 import com.google.i18n.phonenumbers.CppMetadataGenerator.Type; 20 21 import java.io.ByteArrayOutputStream; 22 import java.io.File; 23 import java.io.FileNotFoundException; 24 import java.io.FileOutputStream; 25 import java.io.IOException; 26 import java.io.OutputStream; 27 import java.io.OutputStreamWriter; 28 import java.nio.charset.Charset; 29 import java.util.Arrays; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 33 /** 34 * This class generates the C++ code representation of the provided XML metadata file. It lets us 35 * embed metadata directly in a native binary. We link the object resulting from the compilation of 36 * the code emitted by this class with the C++ phonenumber library. 37 * 38 * @author Philippe Liard 39 * @author David Beaumont 40 */ 41 public class BuildMetadataCppFromXml extends Command { 42 43 /** An enum encapsulating the variations of metadata that we can produce. */ 44 public enum Variant { 45 /** The default 'full' variant which contains all the metadata. */ 46 FULL("%s"), 47 /** The test variant which contains fake data for tests. */ 48 TEST("test_%s"), 49 /** 50 * The lite variant contains the same metadata as the full version but excludes any example 51 * data. This is typically used for clients with space restrictions. 52 */ 53 LITE("lite_%s"); 54 55 private final String template; 56 Variant(String template)57 private Variant(String template) { 58 this.template = template; 59 } 60 61 /** 62 * Returns the basename of the type by adding the name of the current variant. The basename of 63 * a Type is used to determine the name of the source file in which the metadata is defined. 64 * 65 * <p>Note that when the variant is {@link Variant#FULL} this method just returns the type name. 66 */ getBasename(Type type)67 public String getBasename(Type type) { 68 return String.format(template, type); 69 } 70 71 /** 72 * Parses metadata variant name. By default (for a name of {@code ""} or {@code null}) we return 73 * {@link Variant#FULL}, otherwise we match against the variant name (either "test" or "lite"). 74 */ parse(String variantName)75 public static Variant parse(String variantName) { 76 if ("test".equalsIgnoreCase(variantName)) { 77 return Variant.TEST; 78 } else if ("lite".equalsIgnoreCase(variantName)) { 79 return Variant.LITE; 80 } else if (variantName == null || variantName.length() == 0) { 81 return Variant.FULL; 82 } else { 83 return null; 84 } 85 } 86 } 87 88 /** 89 * An immutable options class for parsing and representing the command line options for this 90 * command. 91 */ 92 // @VisibleForTesting 93 static final class Options { 94 private static final Pattern BASENAME_PATTERN = 95 Pattern.compile("(?:(test|lite)_)?([a-z_]+)"); 96 parse(String commandName, String[] args)97 public static Options parse(String commandName, String[] args) { 98 if (args.length == 4) { 99 String inputXmlFilePath = args[1]; 100 String outputDirPath = args[2]; 101 Matcher basenameMatcher = BASENAME_PATTERN.matcher(args[3]); 102 if (basenameMatcher.matches()) { 103 Variant variant = Variant.parse(basenameMatcher.group(1)); 104 Type type = Type.parse(basenameMatcher.group(2)); 105 if (type != null && variant != null) { 106 return new Options(inputXmlFilePath, outputDirPath, type, variant); 107 } 108 } 109 } 110 throw new IllegalArgumentException(String.format( 111 "Usage: %s <inputXmlFile> <outputDir> ( <type> | test_<type> | lite_<type> )\n" + 112 " where <type> is one of: %s", 113 commandName, Arrays.asList(Type.values()))); 114 } 115 116 // File path where the XML input can be found. 117 private final String inputXmlFilePath; 118 // Output directory where the generated files will be saved. 119 private final String outputDirPath; 120 private final Type type; 121 private final Variant variant; 122 Options(String inputXmlFilePath, String outputDirPath, Type type, Variant variant)123 private Options(String inputXmlFilePath, String outputDirPath, Type type, Variant variant) { 124 this.inputXmlFilePath = inputXmlFilePath; 125 this.outputDirPath = outputDirPath; 126 this.type = type; 127 this.variant = variant; 128 } 129 getInputFilePath()130 public String getInputFilePath() { 131 return inputXmlFilePath; 132 } 133 getOutputDir()134 public String getOutputDir() { 135 return outputDirPath; 136 } 137 getType()138 public Type getType() { 139 return type; 140 } 141 getVariant()142 public Variant getVariant() { 143 return variant; 144 } 145 } 146 147 @Override getCommandName()148 public String getCommandName() { 149 return "BuildMetadataCppFromXml"; 150 } 151 152 /** 153 * Generates C++ header and source files to represent the metadata specified by this command's 154 * arguments. The metadata XML file is read and converted to a byte array before being written 155 * into a C++ source file as a static data array. 156 * 157 * @return true if the generation succeeded. 158 */ 159 @Override start()160 public boolean start() { 161 try { 162 Options opt = Options.parse(getCommandName(), getArgs()); 163 byte[] data = loadMetadataBytes(opt.getInputFilePath(), opt.getVariant() == Variant.LITE); 164 CppMetadataGenerator metadata = CppMetadataGenerator.create(opt.getType(), data); 165 166 // TODO: Consider adding checking for correctness of file paths and access. 167 OutputStream headerStream = null; 168 OutputStream sourceStream = null; 169 try { 170 File dir = new File(opt.getOutputDir()); 171 headerStream = openHeaderStream(dir, opt.getType()); 172 sourceStream = openSourceStream(dir, opt.getType(), opt.getVariant()); 173 metadata.outputHeaderFile(new OutputStreamWriter(headerStream, UTF_8)); 174 metadata.outputSourceFile(new OutputStreamWriter(sourceStream, UTF_8)); 175 } finally { 176 FileUtils.closeFiles(headerStream, sourceStream); 177 } 178 return true; 179 } catch (IOException e) { 180 System.err.println(e.getMessage()); 181 } catch (RuntimeException e) { 182 System.err.println(e.getMessage()); 183 } 184 return false; 185 } 186 187 /** Loads the metadata XML file and converts its contents to a byte array. */ loadMetadataBytes(String inputFilePath, boolean liteMetadata)188 private byte[] loadMetadataBytes(String inputFilePath, boolean liteMetadata) { 189 ByteArrayOutputStream out = new ByteArrayOutputStream(); 190 try { 191 writePhoneMetadataCollection(inputFilePath, liteMetadata, out); 192 } catch (Exception e) { 193 // We cannot recover from any exceptions thrown here, so promote them to runtime exceptions. 194 throw new RuntimeException(e); 195 } finally { 196 FileUtils.closeFiles(out); 197 } 198 return out.toByteArray(); 199 } 200 201 // @VisibleForTesting writePhoneMetadataCollection( String inputFilePath, boolean liteMetadata, OutputStream out)202 void writePhoneMetadataCollection( 203 String inputFilePath, boolean liteMetadata, OutputStream out) throws IOException, Exception { 204 BuildMetadataFromXml.buildPhoneMetadataCollection(inputFilePath, liteMetadata, false) 205 .writeTo(out); 206 } 207 208 // @VisibleForTesting openHeaderStream(File dir, Type type)209 OutputStream openHeaderStream(File dir, Type type) throws FileNotFoundException { 210 return new FileOutputStream(new File(dir, type + ".h")); 211 } 212 213 // @VisibleForTesting openSourceStream(File dir, Type type, Variant variant)214 OutputStream openSourceStream(File dir, Type type, Variant variant) throws FileNotFoundException { 215 return new FileOutputStream(new File(dir, variant.getBasename(type) + ".cc")); 216 } 217 218 /** The charset in which our source and header files will be written. */ 219 private static final Charset UTF_8 = Charset.forName("UTF-8"); 220 } 221