blob: ef7dd86d2aa32e778bca6e675a80b5cc17ea8cbf [file] [log] [blame]
/*
* 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.android.i18n.addressinput;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* API to access address verification data used to verify components of an address. Each API
* implements a different form of verification. <p> Not all fields require all types of validation,
* although the APIs are designed so that this could be done. In particular, the current
* implementation only provides known value verification for the hierarchical fields, and only
* provides format and match verification for the postal code field.
*/
public class FieldVerifier {
// Assumes root, will be reset to false when initializing with parent.
private boolean isRoot = true;
private String id;
private DataSource data;
private Set<AddressField> used;
private Set<AddressField> required;
// Known values. Can be either a key, a name in Latin, or a name in native script.
private Map<String, String> known;
private String[] keys = {};
// Names in Latin.
private String[] lnames;
// Names in native script.
private String[] names;
private Pattern format;
private Pattern match;
public FieldVerifier(DataSource data) {
used = new HashSet<AddressField>();
required = new HashSet<AddressField>();
id = "data";
this.data = data;
AddressVerificationNodeData rootNode = data.getDefaultData("data");
populate(rootNode);
known = Util.buildNameToKeyMap(keys, null, null);
}
public FieldVerifier(FieldVerifier parent, AddressVerificationNodeData nodeData) {
isRoot = false;
used = parent.used;
required = parent.required;
data = parent.data;
format = parent.format;
match = parent.match;
known = parent.known;
populate(nodeData);
if (known == null) {
known = new HashMap<String, String>();
}
known.putAll(Util.buildNameToKeyMap(keys, names, lnames));
}
private void populate(AddressVerificationNodeData nodeData) {
if (nodeData.containsKey(AddressDataKey.ID)) {
id = nodeData.get(AddressDataKey.ID);
}
if (nodeData.containsKey(AddressDataKey.COUNTRIES)) {
keys = nodeData.get(AddressDataKey.COUNTRIES).split("~");
}
if (nodeData.containsKey(AddressDataKey.XZIP)) {
format = Pattern.compile(nodeData.get(AddressDataKey.XZIP));
}
if (nodeData.containsKey(AddressDataKey.SUB_KEYS)) {
keys = nodeData.get(AddressDataKey.SUB_KEYS).split("~");
}
if (nodeData.containsKey(AddressDataKey.SUB_LNAMES)) {
lnames = nodeData.get(AddressDataKey.SUB_LNAMES).split("~");
}
if (nodeData.containsKey(AddressDataKey.SUB_NAMES)) {
names = nodeData.get(AddressDataKey.SUB_NAMES).split("~");
}
if (nodeData.containsKey(AddressDataKey.FMT)) {
used = parseAddressFields(nodeData.get(AddressDataKey.FMT));
}
if (nodeData.containsKey(AddressDataKey.REQUIRE)) {
required = parseRequireString(nodeData.get(AddressDataKey.REQUIRE));
}
if (nodeData.containsKey(AddressDataKey.ZIP)) {
if (isCountryKey()) {
format = Pattern.compile(nodeData.get(AddressDataKey.ZIP));
} else {
match = Pattern.compile(nodeData.get(AddressDataKey.ZIP));
}
}
if (keys != null && names == null && lnames != null && keys.length == lnames.length) {
names = keys;
}
}
FieldVerifier refineVerifier(String sublevel) {
String subLevelName = id + "/" + sublevel;
AddressVerificationNodeData nodeData = data.get(subLevelName);
if (nodeData == null) {
if (lnames != null) {
int n = 0;
boolean found = false;
while (n < lnames.length) {
if (lnames[n].equalsIgnoreCase(sublevel)) {
found = true;
break;
}
n++;
}
if (!found) {
return this;
}
subLevelName = id + "/" + names[n];
nodeData = data.get(subLevelName);
if (nodeData == null) {
return this;
} else {
return new FieldVerifier(this, nodeData);
}
}
return this;
}
return new FieldVerifier(this, nodeData);
}
private void report(AddressProblemType problem, AddressField field, String value,
AddressProblems problems) {
problems.add(field, problem);
}
protected boolean check(LookupKey.ScriptType script, AddressProblemType problem,
AddressField field, String value, AddressProblems problems) {
// Assumes no problem found
boolean problemFound = false;
String trimmedValue = Util.trimToNull(value);
switch (problem) {
case UNUSED_FIELD:
if (trimmedValue != null && !used.contains(field)) {
problemFound = true;
}
break;
case MISSING_REQUIRED_FIELD:
if (required.contains(field) && trimmedValue == null) {
problemFound = true;
}
break;
case UNKNOWN_VALUE:
// An empty string will never be an UNKNOWN_VALUE. It is invalid
// only when it appears in a required field (In that case it will
// be reported as MISSING_REQUIRED_FIELD).
if (trimmedValue == null) {
break;
}
// Hack will be fixed later
// problemFound = !isKnownInScript(script, value);
break;
case UNRECOGNIZED_FORMAT:
if (trimmedValue != null && format != null &&
!format.matcher(value == null ? "" : value).matches()) {
problemFound = true;
}
break;
case MISMATCHING_VALUE:
if (trimmedValue != null && match != null &&
!match.matcher(value == null ? "" : value).lookingAt()) {
problemFound = true;
}
break;
default:
throw new RuntimeException("unknown problem: " + problem);
}
if (problemFound) {
problems.add(field, problem);
}
return !problemFound;
}
private boolean isKnownInScript(LookupKey.ScriptType script, String value) {
String trimmedValue = Util.trimToNull(value);
Util.checkNotNull(trimmedValue);
if (script == null) {
return known == null || known.containsKey(trimmedValue.toLowerCase());
}
String[] currNames =
script == LookupKey.ScriptType.LATIN ? lnames : names;
Set<String> candidates = new HashSet<String>();
if (currNames != null) {
for (String name : currNames) {
candidates.add(name.toLowerCase());
}
}
if (keys != null) {
for (String name : keys) {
candidates.add(name.toLowerCase());
}
}
if (candidates.size() == 0 || trimmedValue == null) {
return true;
}
return candidates.contains(value.toLowerCase());
}
public String toString() {
return id;
}
// Util methods for parsing node content.
private static Set<AddressField> parseAddressFields(String value) {
EnumSet<AddressField> result = EnumSet.of(AddressField.COUNTRY);
boolean escaped = false;
for (char c : value.toCharArray()) {
if (escaped) {
escaped = false;
if (c == 'n') {
continue;
}
AddressField f = AddressField.of(c);
if (f == null) {
throw new RuntimeException(
"Unrecognized character '" + c + "' in format pattern: "
+ value);
}
result.add(f);
} else if (c == '%') {
escaped = true;
}
}
// TODO: Investigate this.
// For some reason Address 1 & 2 is removed if they are specified as fields.
// I assume this is due to errors in data, but I don't know.
result.remove(AddressField.ADDRESS_LINE_1);
result.remove(AddressField.ADDRESS_LINE_2);
return result;
}
private static Set<AddressField> parseRequireString(String value) {
// country is always required
EnumSet<AddressField> result = EnumSet.of(AddressField.COUNTRY);
for (char c : value.toCharArray()) {
AddressField f = AddressField.of(c);
if (f == null) {
throw new RuntimeException("Unrecognized character '" + c + "' in require pattern: "
+ value);
}
result.add(f);
}
// TODO: Investigate this.
// For some reason Address 1 & 2 is removed if they are specified as required.
// I assume this is due to errors in data, but I don't know.
result.remove(AddressField.ADDRESS_LINE_1);
result.remove(AddressField.ADDRESS_LINE_2);
return result;
}
private boolean isCountryKey() {
Util.checkNotNull(id, "cannot use null as key");
return id.split("/").length == 2;
}
}