1/* 2 * Copyright (C) 2010 Google Inc. 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 17package com.android.i18n.addressinput; 18 19import com.android.i18n.addressinput.LookupKey.ScriptType; 20 21import org.json.JSONException; 22import org.json.JSONObject; 23import org.json.JSONTokener; 24 25import java.util.ArrayList; 26import java.util.Collections; 27import java.util.Comparator; 28import java.util.HashMap; 29import java.util.List; 30import java.util.Map; 31 32/** 33 * Address format interpreter. A utility to find address format related info. 34 */ 35class FormatInterpreter { 36 37 private static final String NEW_LINE = "%n"; 38 39 private final String mDefaultFormat; 40 41 private final FormOptions mFormOptions; 42 43 /** 44 * Creates a new instance of {@link FormatInterpreter}. 45 */ 46 FormatInterpreter(FormOptions options) { 47 Util.checkNotNull(RegionDataConstants.getCountryFormatMap(), 48 "null country name map not allowed"); 49 Util.checkNotNull(options); 50 mFormOptions = options; 51 mDefaultFormat = getJsonValue("ZZ", AddressDataKey.FMT); 52 Util.checkNotNull(mDefaultFormat, "null default format not allowed"); 53 } 54 55 /** 56 * Returns a list of address fields based on the format of {@code regionCode}. Script type is 57 * needed because some countries uses different address formats for local/Latin scripts. 58 * 59 * @param scriptType if {@link ScriptType#LOCAL}, use local format; else use Latin format. 60 */ 61 List<AddressField> getAddressFieldOrder(ScriptType scriptType, String regionCode) { 62 Util.checkNotNull(scriptType); 63 Util.checkNotNull(regionCode); 64 List<AddressField> fieldOrder = new ArrayList<AddressField>(); 65 for (String substring : getFormatSubStrings(scriptType, regionCode)) { 66 // Skips un-escaped characters and new lines. 67 if (!substring.matches("%.") || substring.equals(NEW_LINE)) { 68 continue; 69 } 70 71 AddressField field = AddressField.of(substring.charAt(1)); 72 fieldOrder.add(field); 73 } 74 75 overrideFieldOrder(regionCode, fieldOrder); 76 77 // Uses two address lines instead of street address. 78 List<AddressField> finalFieldOrder = new ArrayList<AddressField>(); 79 for (AddressField field : fieldOrder) { 80 if (field == AddressField.STREET_ADDRESS) { 81 finalFieldOrder.add(AddressField.ADDRESS_LINE_1); 82 finalFieldOrder.add(AddressField.ADDRESS_LINE_2); 83 } else { 84 finalFieldOrder.add(field); 85 } 86 } 87 return finalFieldOrder; 88 } 89 90 /** 91 * Returns a list of address fields based on the format of {@code regionCode} -- assuming script 92 * type is {@link ScriptType#LOCAL}. 93 */ 94 List<AddressField> getAddressFieldOrder(String regionCode) { 95 Util.checkNotNull(regionCode); 96 return getAddressFieldOrder(ScriptType.LOCAL, regionCode); 97 } 98 99 private void overrideFieldOrder(String regionCode, List<AddressField> fieldOrder) { 100 if (mFormOptions.getCustomFieldOrder(regionCode) == null) { 101 return; 102 } 103 104 // Constructs a hash for overridden field order. 105 final Map<AddressField, Integer> fieldPriority = new HashMap<AddressField, Integer>(); 106 int i = 0; 107 for (AddressField field : mFormOptions.getCustomFieldOrder(regionCode)) { 108 fieldPriority.put(field, i); 109 i++; 110 } 111 112 // Finds union of input fields and priority list. 113 List<AddressField> union = new ArrayList<AddressField>(); 114 List<Integer> slots = new ArrayList<Integer>(); 115 i = 0; 116 for (AddressField field : fieldOrder) { 117 if (fieldPriority.containsKey(field)) { 118 union.add(field); 119 slots.add(i); 120 } 121 i++; 122 } 123 124 // Overrides field order with priority list. 125 Collections.sort(union, new Comparator<AddressField>() { 126 @Override 127 public int compare(AddressField o1, AddressField o2) { 128 return fieldPriority.get(o1) - fieldPriority.get(o2); 129 } 130 }); 131 132 // Puts reordered fields in slots. 133 for (int j = 0; j < union.size(); ++j) { 134 fieldOrder.set(slots.get(j), union.get(j)); 135 } 136 } 137 138 /** 139 * Gets formatted address. For example, 140 * 141 * <p> John Doe<br> Dnar Corp<br> 5th St<br> Santa Monica CA 90123 </p> 142 * 143 * This method does not validate addresses. Also, it will "normalize" the result strings by 144 * removing redundant spaces and empty lines. 145 */ 146 List<String> getEnvelopeAddress(AddressData address) { 147 Util.checkNotNull(address, "null input address not allowed"); 148 String regionCode = address.getPostalCountry(); 149 150 String lc = address.getLanguageCode(); 151 ScriptType scriptType = ScriptType.LOCAL; 152 if (lc != null) { 153 scriptType = Util.isExplicitLatinScript(lc) ? ScriptType.LATIN : ScriptType.LOCAL; 154 } 155 156 List<String> lines = new ArrayList<String>(); 157 StringBuilder currentLine = new StringBuilder(); 158 for (String formatSymbol : getFormatSubStrings(scriptType, regionCode)) { 159 if (formatSymbol.equals(NEW_LINE)) { 160 String normalizedStr = 161 removeRedundantSpacesAndLeadingPunctuation(currentLine.toString()); 162 if (normalizedStr.length() > 0) { 163 lines.add(normalizedStr); 164 currentLine.setLength(0); 165 } 166 } else if (formatSymbol.startsWith("%")) { 167 char c = formatSymbol.charAt(1); 168 AddressField field = AddressField.of(c); 169 Util.checkNotNull(field, "null address field for character " + c); 170 171 String value = null; 172 switch (field) { 173 case STREET_ADDRESS: 174 value = Util.joinAndSkipNulls("\n", 175 address.getAddressLine1(), 176 address.getAddressLine2()); 177 break; 178 case COUNTRY: 179 // Country name is treated separately. 180 break; 181 case ADMIN_AREA: 182 value = address.getAdministrativeArea(); 183 break; 184 case LOCALITY: 185 value = address.getLocality(); 186 break; 187 case DEPENDENT_LOCALITY: 188 value = address.getDependentLocality(); 189 break; 190 case RECIPIENT: 191 value = address.getRecipient(); 192 break; 193 case ORGANIZATION: 194 value = address.getOrganization(); 195 break; 196 case POSTAL_CODE: 197 value = address.getPostalCode(); 198 break; 199 default: 200 break; 201 } 202 203 if (value != null) { 204 currentLine.append(value); 205 } 206 } else { 207 currentLine.append(formatSymbol); 208 } 209 } 210 String normalizedStr = removeRedundantSpacesAndLeadingPunctuation(currentLine.toString()); 211 if (normalizedStr.length() > 0) { 212 lines.add(normalizedStr); 213 } 214 return lines; 215 } 216 217 /** 218 * Tokenizes the format string and returns the token string list. "%" is treated as an escape 219 * character. So for example "%n%a%nxyz" will be split into "%n", "%a", "%n", "x", "y", and "z". 220 * Escaped tokens correspond to either new line or address fields. 221 */ 222 private List<String> getFormatSubStrings(ScriptType scriptType, String regionCode) { 223 String formatString = getFormatString(scriptType, regionCode); 224 List<String> parts = new ArrayList<String>(); 225 226 boolean escaped = false; 227 for (char c : formatString.toCharArray()) { 228 if (escaped) { 229 escaped = false; 230 if (NEW_LINE.equals("%" + c)) { 231 parts.add(NEW_LINE); 232 } else { 233 Util.checkNotNull(AddressField.of(c), "Unrecognized character '" + c 234 + "' in format pattern: " + formatString); 235 parts.add("%" + c); 236 } 237 } else if (c == '%') { 238 escaped = true; 239 } else { 240 parts.add(c + ""); 241 } 242 } 243 return parts; 244 } 245 246 private static String removeRedundantSpacesAndLeadingPunctuation(String str) { 247 // Remove leading commas and other punctuation that might have been added by the formatter 248 // in the case of missing data. 249 str = str.replaceFirst("^[-,\\s]+", ""); 250 str = str.trim(); 251 str = str.replaceAll(" +", " "); 252 return str; 253 } 254 255 private static String getFormatString(ScriptType scriptType, String regionCode) { 256 String format = (scriptType == ScriptType.LOCAL) 257 ? getJsonValue(regionCode, AddressDataKey.FMT) 258 : getJsonValue(regionCode, AddressDataKey.LFMT); 259 if (format == null) { 260 format = getJsonValue("ZZ", AddressDataKey.FMT); 261 } 262 return format; 263 } 264 265 private static String getJsonValue(String regionCode, AddressDataKey key) { 266 Util.checkNotNull(regionCode); 267 String jsonString = RegionDataConstants.getCountryFormatMap().get(regionCode); 268 Util.checkNotNull(jsonString, "no json data for region code " + regionCode); 269 270 try { 271 JSONObject jsonObj = new JSONObject(new JSONTokener(jsonString)); 272 if (!jsonObj.has(key.name().toLowerCase())) { 273 // Key not found. Return null. 274 return null; 275 } 276 // Gets the string for this key. 277 String parsedJsonString = jsonObj.getString(key.name().toLowerCase()); 278 return parsedJsonString; 279 } catch (JSONException e) { 280 throw new RuntimeException("Invalid json for region code " + regionCode 281 + ": " + jsonString); 282 } 283 } 284} 285