/*
* Copyright 2008 ZXing authors
*
* 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.zxing.client.result;
import com.google.zxing.Result;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Vector;
/**
* Parses contact information formatted according to the VCard (2.1) format. This is not a complete
* implementation but should parse information as commonly encoded in 2D barcodes.
*
* @author Sean Owen
*/
final class VCardResultParser extends ResultParser {
private VCardResultParser() {
}
public static AddressBookParsedResult parse(Result result) {
// Although we should insist on the raw text ending with "END:VCARD", there's no reason
// to throw out everything else we parsed just because this was omitted. In fact, Eclair
// is doing just that, and we can't parse its contacts without this leniency.
String rawText = result.getText();
if (rawText == null || !rawText.startsWith("BEGIN:VCARD")) {
return null;
}
String[] names = matchVCardPrefixedField("FN", rawText, true);
if (names == null) {
// If no display names found, look for regular name fields and format them
names = matchVCardPrefixedField("N", rawText, true);
formatNames(names);
}
String[] phoneNumbers = matchVCardPrefixedField("TEL", rawText, true);
String[] emails = matchVCardPrefixedField("EMAIL", rawText, true);
String note = matchSingleVCardPrefixedField("NOTE", rawText, false);
String[] addresses = matchVCardPrefixedField("ADR", rawText, true);
if (addresses != null) {
for (int i = 0; i < addresses.length; i++) {
addresses[i] = formatAddress(addresses[i]);
}
}
String org = matchSingleVCardPrefixedField("ORG", rawText, true);
String birthday = matchSingleVCardPrefixedField("BDAY", rawText, true);
if (!isLikeVCardDate(birthday)) {
birthday = null;
}
String title = matchSingleVCardPrefixedField("TITLE", rawText, true);
String url = matchSingleVCardPrefixedField("URL", rawText, true);
return new AddressBookParsedResult(names, null, phoneNumbers, emails, note, addresses, org,
birthday, title, url);
}
private static String[] matchVCardPrefixedField(String prefix, String rawText, boolean trim) {
Vector matches = null;
int i = 0;
int max = rawText.length();
while (i < max) {
i = rawText.indexOf(prefix, i);
if (i < 0) {
break;
}
if (i > 0 && rawText.charAt(i - 1) != '\n') {
// then this didn't start a new token, we matched in the middle of something
i++;
continue;
}
i += prefix.length(); // Skip past this prefix we found to start
if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') {
continue;
}
int metadataStart = i;
while (rawText.charAt(i) != ':') { // Skip until a colon
i++;
}
boolean quotedPrintable = false;
String quotedPrintableCharset = null;
if (i > metadataStart) {
// There was something after the tag, before colon
int j = metadataStart+1;
while (j <= i) {
if (rawText.charAt(j) == ';' || rawText.charAt(j) == ':') {
String metadata = rawText.substring(metadataStart+1, j);
int equals = metadata.indexOf('=');
if (equals >= 0) {
String key = metadata.substring(0, equals);
String value = metadata.substring(equals+1);
if ("ENCODING".equalsIgnoreCase(key)) {
if ("QUOTED-PRINTABLE".equalsIgnoreCase(value)) {
quotedPrintable = true;
}
} else if ("CHARSET".equalsIgnoreCase(key)) {
quotedPrintableCharset = value;
}
}
metadataStart = j;
}
j++;
}
}
i++; // skip colon
int matchStart = i; // Found the start of a match here
while ((i = rawText.indexOf((int) '\n', i)) >= 0) { // Really, end in \r\n
if (i < rawText.length() - 1 && // But if followed by tab or space,
(rawText.charAt(i+1) == ' ' || // this is only a continuation
rawText.charAt(i+1) == '\t')) {
i += 2; // Skip \n and continutation whitespace
} else if (quotedPrintable && // If preceded by = in quoted printable
(rawText.charAt(i-1) == '=' || // this is a continuation
rawText.charAt(i-2) == '=')) {
i++; // Skip \n
} else {
break;
}
}
if (i < 0) {
// No terminating end character? uh, done. Set i such that loop terminates and break
i = max;
} else if (i > matchStart) {
// found a match
if (matches == null) {
matches = new Vector(1); // lazy init
}
if (rawText.charAt(i-1) == '\r') {
i--; // Back up over \r, which really should be there
}
String element = rawText.substring(matchStart, i);
if (trim) {
element = element.trim();
}
if (quotedPrintable) {
element = decodeQuotedPrintable(element, quotedPrintableCharset);
} else {
element = stripContinuationCRLF(element);
}
matches.addElement(element);
i++;
} else {
i++;
}
}
if (matches == null || matches.isEmpty()) {
return null;
}
return toStringArray(matches);
}
private static String stripContinuationCRLF(String value) {
int length = value.length();
StringBuffer result = new StringBuffer(length);
boolean lastWasLF = false;
for (int i = 0; i < length; i++) {
if (lastWasLF) {
lastWasLF = false;
continue;
}
char c = value.charAt(i);
lastWasLF = false;
switch (c) {
case '\n':
lastWasLF = true;
break;
case '\r':
break;
default:
result.append(c);
}
}
return result.toString();
}
private static String decodeQuotedPrintable(String value, String charset) {
int length = value.length();
StringBuffer result = new StringBuffer(length);
ByteArrayOutputStream fragmentBuffer = new ByteArrayOutputStream();
for (int i = 0; i < length; i++) {
char c = value.charAt(i);
switch (c) {
case '\r':
case '\n':
break;
case '=':
if (i < length - 2) {
char nextChar = value.charAt(i+1);
if (nextChar == '\r' || nextChar == '\n') {
// Ignore, it's just a continuation symbol
} else {
char nextNextChar = value.charAt(i+2);
try {
int encodedByte = 16 * toHexValue(nextChar) + toHexValue(nextNextChar);
fragmentBuffer.write(encodedByte);
} catch (IllegalArgumentException iae) {
// continue, assume it was incorrectly encoded
}
i += 2;
}
}
break;
default:
maybeAppendFragment(fragmentBuffer, charset, result);
result.append(c);
}
}
maybeAppendFragment(fragmentBuffer, charset, result);
return result.toString();
}
private static int toHexValue(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
throw new IllegalArgumentException();
}
private static void maybeAppendFragment(ByteArrayOutputStream fragmentBuffer,
String charset,
StringBuffer result) {
if (fragmentBuffer.size() > 0) {
byte[] fragmentBytes = fragmentBuffer.toByteArray();
String fragment;
if (charset == null) {
fragment = new String(fragmentBytes);
} else {
try {
fragment = new String(fragmentBytes, charset);
} catch (UnsupportedEncodingException e) {
// Yikes, well try anyway:
fragment = new String(fragmentBytes);
}
}
fragmentBuffer.reset();
result.append(fragment);
}
}
static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) {
String[] values = matchVCardPrefixedField(prefix, rawText, trim);
return values == null ? null : values[0];
}
private static boolean isLikeVCardDate(String value) {
if (value == null) {
return true;
}
// Not really sure this is true but matches practice
// Mach YYYYMMDD
if (isStringOfDigits(value, 8)) {
return true;
}
// or YYYY-MM-DD
return
value.length() == 10 &&
value.charAt(4) == '-' &&
value.charAt(7) == '-' &&
isSubstringOfDigits(value, 0, 4) &&
isSubstringOfDigits(value, 5, 2) &&
isSubstringOfDigits(value, 8, 2);
}
private static String formatAddress(String address) {
if (address == null) {
return null;
}
int length = address.length();
StringBuffer newAddress = new StringBuffer(length);
for (int j = 0; j < length; j++) {
char c = address.charAt(j);
if (c == ';') {
newAddress.append(' ');
} else {
newAddress.append(c);
}
}
return newAddress.toString().trim();
}
/**
* Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like
* "Reverend John Q. Public III".
*
* @param names name values to format, in place
*/
private static void formatNames(String[] names) {
if (names != null) {
for (int i = 0; i < names.length; i++) {
String name = names[i];
String[] components = new String[5];
int start = 0;
int end;
int componentIndex = 0;
while ((end = name.indexOf(';', start)) > 0) {
components[componentIndex] = name.substring(start, end);
componentIndex++;
start = end + 1;
}
components[componentIndex] = name.substring(start);
StringBuffer newName = new StringBuffer(100);
maybeAppendComponent(components, 3, newName);
maybeAppendComponent(components, 1, newName);
maybeAppendComponent(components, 2, newName);
maybeAppendComponent(components, 0, newName);
maybeAppendComponent(components, 4, newName);
names[i] = newName.toString().trim();
}
}
}
private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) {
if (components[i] != null) {
newName.append(' ');
newName.append(components[i]);
}
}
}