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