Support SMTP URLs
[zxing.git] / core / src / com / google / zxing / client / result / VCardResultParser.java
index 834db4c..4d53971 100644 (file)
@@ -18,54 +18,68 @@ 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
  */
-public final class VCardResultParser extends ResultParser {
+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") || !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 (rawText.charAt(i - 1) != '\n') {
+
+      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;
@@ -74,40 +88,206 @@ public 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);
   }
 
-  private 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 @@ public 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 @@ public final class VCardResultParser extends ResultParser {
     }
   }
 
-}
\ No newline at end of file
+}