X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=core%2Fsrc%2Fcom%2Fgoogle%2Fzxing%2Fclient%2Fresult%2FVCardResultParser.java;h=4d53971396d504300320fa82b78a127930016319;hb=10276c1fd6c1871565ad6d6c87f0249f1c0b7e5c;hp=c0717dbebfc14580b260a260207165764b4272b7;hpb=4166bfcc94c0aa6365be15b73aa7dda767ef240a;p=zxing.git diff --git a/core/src/com/google/zxing/client/result/VCardResultParser.java b/core/src/com/google/zxing/client/result/VCardResultParser.java index c0717dbe..4d539713 100644 --- a/core/src/com/google/zxing/client/result/VCardResultParser.java +++ b/core/src/com/google/zxing/client/result/VCardResultParser.java @@ -18,13 +18,15 @@ 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 srowen@google.com (Sean Owen) + * @author Sean Owen */ final class VCardResultParser extends ResultParser { @@ -32,39 +34,51 @@ final class VCardResultParser extends ResultParser { } 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") || !rawText.endsWith("END:VCARD")) { + if (rawText == null || !rawText.startsWith("BEGIN:VCARD")) { return null; } - String[] names = matchVCardPrefixedField("FN", rawText); + 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); + names = matchVCardPrefixedField("N", rawText, true); formatNames(names); } - String[] phoneNumbers = matchVCardPrefixedField("TEL", rawText); - String[] emails = matchVCardPrefixedField("EMAIL", rawText); - String note = matchSingleVCardPrefixedField("NOTE", rawText); - String address = matchSingleVCardPrefixedField("ADR", rawText); - address = formatAddress(address); - String org = matchSingleVCardPrefixedField("ORG", rawText); - String birthday = matchSingleVCardPrefixedField("BDAY", rawText); - if (birthday != null && !isStringOfDigits(birthday, 8)) { - return null; + 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 title = matchSingleVCardPrefixedField("TITLE", rawText); - return new AddressBookParsedResult(names, phoneNumbers, emails, note, address, org, birthday, title); + 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) { + 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++; @@ -74,40 +88,206 @@ final class VCardResultParser extends ResultParser { 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 (key.equalsIgnoreCase("ENCODING")) { + if (value.equalsIgnoreCase("QUOTED-PRINTABLE")) { + quotedPrintable = true; + } + } else if (key.equalsIgnoreCase("CHARSET")) { + quotedPrintableCharset = value; + } + } + metadataStart = j; + } + j++; + } + } + i++; // skip colon - int start = i; // Found the start of a match here - boolean done = false; - while (!done) { - i = rawText.indexOf((int) '\n', i); // Really, ends in \r\n - if (i < 0) { - // No terminating end character? uh, done. Set i such that loop terminates and break - i = rawText.length(); - done = true; + + 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 { - // found a match - if (matches == null) { - matches = new Vector(3); // lazy init - } - matches.addElement(rawText.substring(start, i - 1)); // i - 1 to strip off the \r too - i++; - done = true; + 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); } - static String matchSingleVCardPrefixedField(String prefix, String rawText) { - String[] values = matchVCardPrefixedField(prefix, rawText); + 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; @@ -145,7 +325,7 @@ final class VCardResultParser extends ResultParser { start = end + 1; } components[componentIndex] = name.substring(start); - StringBuffer newName = new StringBuffer(); + StringBuffer newName = new StringBuffer(100); maybeAppendComponent(components, 3, newName); maybeAppendComponent(components, 1, newName); maybeAppendComponent(components, 2, newName); @@ -163,4 +343,4 @@ final class VCardResultParser extends ResultParser { } } -} \ No newline at end of file +}