Java tutorial
/* * Copyright (C) 2010 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.addressinput.common; import com.google.i18n.addressinput.common.AddressField.WidthType; import com.google.i18n.addressinput.common.LookupKey.ScriptType; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Address format interpreter. A utility to find address format related info. */ public final class FormatInterpreter { private static final String NEW_LINE = "%n"; private final FormOptions.Snapshot formOptions; /** * Creates a new instance of {@link FormatInterpreter}. */ public FormatInterpreter(FormOptions.Snapshot options) { Util.checkNotNull(RegionDataConstants.getCountryFormatMap(), "null country name map not allowed"); Util.checkNotNull(options); this.formOptions = options; Util.checkNotNull(getJsonValue("ZZ", AddressDataKey.FMT), "Could not obtain a default address field order."); } /** * Returns a list of address fields based on the format of {@code regionCode}. Script type is * needed because some countries uses different address formats for local/Latin scripts. * * @param scriptType if {@link ScriptType#LOCAL}, use local format; else use Latin format. */ // TODO: Consider not re-doing this work every time the widget is re-rendered. @SuppressWarnings("deprecation") // For legacy address fields. public List<AddressField> getAddressFieldOrder(ScriptType scriptType, String regionCode) { Util.checkNotNull(scriptType); Util.checkNotNull(regionCode); EnumSet<AddressField> visibleFields = EnumSet.noneOf(AddressField.class); List<AddressField> fieldOrder = new ArrayList<AddressField>(); // TODO: Change this to just enumerate the address fields directly. for (String substring : getFormatSubStrings(scriptType, regionCode)) { // Skips un-escaped characters and new lines. if (!substring.matches("%.") || substring.equals(NEW_LINE)) { continue; } AddressField field = AddressField.of(substring.charAt(1)); // Accept only the first instance for any duplicate fields (which can occur because the // string we start with defines format order, which can contain duplicate fields). if (!visibleFields.contains(field)) { visibleFields.add(field); fieldOrder.add(field); } } applyFieldOrderOverrides(regionCode, fieldOrder); // Uses two address lines instead of street address. for (int n = 0; n < fieldOrder.size(); n++) { if (fieldOrder.get(n) == AddressField.STREET_ADDRESS) { fieldOrder.set(n, AddressField.ADDRESS_LINE_1); fieldOrder.add(n + 1, AddressField.ADDRESS_LINE_2); break; } } return Collections.unmodifiableList(fieldOrder); } private void applyFieldOrderOverrides(String regionCode, List<AddressField> fieldOrder) { List<AddressField> customFieldOrder = formOptions.getCustomFieldOrder(regionCode); if (customFieldOrder == null) { return; } // We can assert that fieldOrder and customFieldOrder contain no duplicates. // We know this by the construction above and in FormOptions but we still have to think // about fields in the custom ordering which aren't visible (the loop below will fail if // a non-visible field appears in the custom ordering). However in that case it's safe to // just ignore the extraneous field. Set<AddressField> nonVisibleCustomFields = EnumSet.copyOf(customFieldOrder); nonVisibleCustomFields.removeAll(fieldOrder); if (nonVisibleCustomFields.size() > 0) { // Local mutable copy to remove non visible fields - this shouldn't happen often. customFieldOrder = new ArrayList<AddressField>(customFieldOrder); customFieldOrder.removeAll(nonVisibleCustomFields); } // It is vital for this loop to work correctly that every element in customFieldOrder // appears in fieldOrder exactly once. for (int fieldIdx = 0, customIdx = 0; fieldIdx < fieldOrder.size(); fieldIdx++) { if (customFieldOrder.contains(fieldOrder.get(fieldIdx))) { fieldOrder.set(fieldIdx, customFieldOrder.get(customIdx++)); } } } /** * Returns the fields that are required to be filled in for this country. This is based upon the * "required" field in RegionDataConstants for {@code regionCode}, and handles falling back to * the default data if necessary. */ static Set<AddressField> getRequiredFields(String regionCode) { Util.checkNotNull(regionCode); String requireString = getRequiredString(regionCode); EnumSet<AddressField> required = EnumSet.of(AddressField.COUNTRY); for (char c : requireString.toCharArray()) { required.add(AddressField.of(c)); } return required; } private static String getRequiredString(String regionCode) { String required = getJsonValue(regionCode, AddressDataKey.REQUIRE); if (required == null) { required = getJsonValue("ZZ", AddressDataKey.REQUIRE); } return required; } /** * Returns the field width override for the specified country, or null if there's none. This is * based upon the "width_overrides" field in RegionDataConstants for {@code regionCode}. */ static WidthType getWidthOverride(AddressField field, String regionCode) { return getWidthOverride(field, regionCode, RegionDataConstants.getCountryFormatMap()); } /** * Visible for Testing - same as {@link #getWidthOverride(AddressField, String)} but testable with * fake data. */ static WidthType getWidthOverride(AddressField field, String regionCode, Map<String, String> regionDataMap) { Util.checkNotNull(regionCode); String overridesString = getJsonValue(regionCode, AddressDataKey.WIDTH_OVERRIDES, regionDataMap); if (overridesString == null || overridesString.isEmpty()) { return null; } // The field width overrides string starts with a %, so we skip the first one. // Example string: "%C:L%S:S" which is a repeated string of // '<%> field_character <:> width_character'. for (int pos = 0; pos != -1;) { int keyStartIndex = pos + 1; int valueStartIndex = overridesString.indexOf(':', keyStartIndex + 1) + 1; if (valueStartIndex == 0 || valueStartIndex == overridesString.length()) { // Malformed string -- % not followed by ':' or trailing ':' return null; } // Prepare for next iteration. pos = overridesString.indexOf('%', valueStartIndex + 1); if (valueStartIndex != keyStartIndex + 2 || overridesString.charAt(keyStartIndex) != field.getChar()) { // Key is not a high level field (unhandled by this code) or does not match. // Also catches malformed string where key is of zero length (skip, not error). continue; } int valueLength = (pos != -1 ? pos : overridesString.length()) - valueStartIndex; if (valueLength != 1) { // Malformed string -- value has length other than 1 return null; } return WidthType.of(overridesString.charAt(valueStartIndex)); } return null; } /** * Gets formatted address. For example, * * <p> John Doe<br> Dnar Corp<br> 5th St<br> Santa Monica CA 90123 </p> * * This method does not validate addresses. Also, it will "normalize" the result strings by * removing redundant spaces and empty lines. */ public List<String> getEnvelopeAddress(AddressData address) { Util.checkNotNull(address, "null input address not allowed"); String regionCode = address.getPostalCountry(); String lc = address.getLanguageCode(); ScriptType scriptType = ScriptType.LOCAL; if (lc != null) { scriptType = Util.isExplicitLatinScript(lc) ? ScriptType.LATIN : ScriptType.LOCAL; } List<String> lines = new ArrayList<String>(); StringBuilder currentLine = new StringBuilder(); for (String formatSymbol : getFormatSubStrings(scriptType, regionCode)) { if (formatSymbol.equals(NEW_LINE)) { String normalizedStr = removeRedundantSpacesAndLeadingPunctuation(currentLine.toString()); if (normalizedStr.length() > 0) { lines.add(normalizedStr); currentLine.setLength(0); } } else if (formatSymbol.startsWith("%")) { String value = null; switch (AddressField.of(formatSymbol.charAt(1))) { case STREET_ADDRESS: value = Util.joinAndSkipNulls("\n", address.getAddressLine1(), address.getAddressLine2()); break; case COUNTRY: // Country name is treated separately. break; case ADMIN_AREA: value = address.getAdministrativeArea(); break; case LOCALITY: value = address.getLocality(); break; case DEPENDENT_LOCALITY: value = address.getDependentLocality(); break; case RECIPIENT: value = address.getRecipient(); break; case ORGANIZATION: value = address.getOrganization(); break; case POSTAL_CODE: value = address.getPostalCode(); break; default: break; } if (value != null) { currentLine.append(value); } } else { currentLine.append(formatSymbol); } } String normalizedStr = removeRedundantSpacesAndLeadingPunctuation(currentLine.toString()); if (normalizedStr.length() > 0) { lines.add(normalizedStr); } return lines; } /** * Tokenizes the format string and returns the token string list. "%" is treated as an escape * character. So for example "%n%a%nxyz" will be split into "%n", "%a", "%n", "x", "y", and "z". * Escaped tokens correspond to either new line or address fields. The output of this method * may contain duplicates. */ // TODO: Create a common method which does field parsing in one place (there are about 4 other // places in this library where format strings are parsed). private List<String> getFormatSubStrings(ScriptType scriptType, String regionCode) { String formatString = getFormatString(scriptType, regionCode); List<String> parts = new ArrayList<String>(); boolean escaped = false; for (char c : formatString.toCharArray()) { if (escaped) { escaped = false; if (NEW_LINE.equals("%" + c)) { parts.add(NEW_LINE); } else { // Checks that the character is valid. AddressField.of(c); parts.add("%" + c); } } else if (c == '%') { escaped = true; } else { parts.add(c + ""); } } return parts; } private static String removeRedundantSpacesAndLeadingPunctuation(String str) { // Remove leading commas and other punctuation that might have been added by the formatter // in the case of missing data. str = str.replaceFirst("^[-,\\s]+", ""); str = str.trim(); str = str.replaceAll(" +", " "); return str; } private static String getFormatString(ScriptType scriptType, String regionCode) { String format = (scriptType == ScriptType.LOCAL) ? getJsonValue(regionCode, AddressDataKey.FMT) : getJsonValue(regionCode, AddressDataKey.LFMT); if (format == null) { format = getJsonValue("ZZ", AddressDataKey.FMT); } return format; } private static String getJsonValue(String regionCode, AddressDataKey key) { return getJsonValue(regionCode, key, RegionDataConstants.getCountryFormatMap()); } /** * Visible for testing only. */ static String getJsonValue(String regionCode, AddressDataKey key, Map<String, String> regionDataMap) { Util.checkNotNull(regionCode); String jsonString = regionDataMap.get(regionCode); Util.checkNotNull(jsonString, "no json data for region code " + regionCode); try { JSONObject jsonObj = new JSONObject(new JSONTokener(jsonString)); if (!jsonObj.has(Util.toLowerCaseLocaleIndependent(key.name()))) { // Key not found. Return null. return null; } // Gets the string for this key. String parsedJsonString = jsonObj.getString(Util.toLowerCaseLocaleIndependent(key.name())); return parsedJsonString; } catch (JSONException e) { throw new RuntimeException("Invalid json for region code " + regionCode + ": " + jsonString); } } }