2 * Copyright 2008 ZXing authors
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.google.zxing.client.result;
19 import com.google.zxing.Result;
21 import java.util.Vector;
24 * Parses contact information formatted according to the VCard (2.1) format. This is not a complete
25 * implementation but should parse information as commonly encoded in 2D barcodes.
29 final class VCardResultParser extends ResultParser {
31 private VCardResultParser() {
34 public static AddressBookParsedResult parse(Result result) {
35 // Although we should insist on the raw text ending with "END:VCARD", there's no reason
36 // to throw out everything else we parsed just because this was omitted. In fact, Eclair
37 // is doing just that, and we can't parse its contacts without this leniency.
38 String rawText = result.getText();
39 if (rawText == null || !rawText.startsWith("BEGIN:VCARD")) {
42 String[] names = matchVCardPrefixedField("FN", rawText, true);
44 // If no display names found, look for regular name fields and format them
45 names = matchVCardPrefixedField("N", rawText, true);
48 String[] phoneNumbers = matchVCardPrefixedField("TEL", rawText, true);
49 String[] emails = matchVCardPrefixedField("EMAIL", rawText, true);
50 String note = matchSingleVCardPrefixedField("NOTE", rawText, false);
51 String[] addresses = matchVCardPrefixedField("ADR", rawText, true);
52 if (addresses != null) {
53 for (int i = 0; i < addresses.length; i++) {
54 addresses[i] = formatAddress(addresses[i]);
57 String org = matchSingleVCardPrefixedField("ORG", rawText, true);
58 String birthday = matchSingleVCardPrefixedField("BDAY", rawText, true);
59 if (!isLikeVCardDate(birthday)) {
62 String title = matchSingleVCardPrefixedField("TITLE", rawText, true);
63 String url = matchSingleVCardPrefixedField("URL", rawText, true);
64 return new AddressBookParsedResult(names, null, phoneNumbers, emails, note, addresses, org,
65 birthday, title, url);
68 private static String[] matchVCardPrefixedField(String prefix, String rawText, boolean trim) {
69 Vector matches = null;
71 int max = rawText.length();
75 i = rawText.indexOf(prefix, i);
80 if (i > 0 && rawText.charAt(i - 1) != '\n') {
81 // then this didn't start a new token, we matched in the middle of something
85 i += prefix.length(); // Skip past this prefix we found to start
86 if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') {
90 int metadataStart = i;
91 while (rawText.charAt(i) != ':') { // Skip until a colon
95 boolean quotedPrintable = false;
96 if (i > metadataStart) {
97 // There was something after the tag, before colon
98 int j = metadataStart+1;
100 if (rawText.charAt(j) == ';' || rawText.charAt(j) == ':') {
101 String metadata = rawText.substring(metadataStart+1, j);
102 int equals = metadata.indexOf('=');
104 String key = metadata.substring(0, equals);
105 String value = metadata.substring(equals+1);
106 if (key.equalsIgnoreCase("ENCODING")) {
107 if (value.equalsIgnoreCase("QUOTED-PRINTABLE")) {
108 quotedPrintable = true;
120 int matchStart = i; // Found the start of a match here
122 while ((i = rawText.indexOf((int) '\n', i)) >= 0) { // Really, end in \r\n
123 if (i < rawText.length() - 1 && // But if followed by tab or space,
124 (rawText.charAt(i+1) == ' ' || // this is only a continuation
125 rawText.charAt(i+1) == '\t')) {
126 i += 2; // Skip \n and continutation whitespace
127 } else if (quotedPrintable && // If preceded by = in quoted printable
128 (rawText.charAt(i-1) == '=' || // this is a continuation
129 rawText.charAt(i-2) == '=')) {
137 // No terminating end character? uh, done. Set i such that loop terminates and break
139 } else if (i > matchStart) {
141 if (matches == null) {
142 matches = new Vector(1); // lazy init
144 if (rawText.charAt(i-1) == '\r') {
145 i--; // Back up over \r, which really should be there
147 String element = rawText.substring(matchStart, i);
149 element = element.trim();
151 if (quotedPrintable) {
152 element = decodeQuotedPrintable(element);
154 element = stripContinuationCRLF(element);
156 matches.addElement(element);
164 if (matches == null || matches.isEmpty()) {
167 return toStringArray(matches);
170 private static String stripContinuationCRLF(String value) {
171 int length = value.length();
172 StringBuffer result = new StringBuffer(length);
173 boolean lastWasLF = false;
174 for (int i = 0; i < length; i++) {
179 char c = value.charAt(i);
191 return result.toString();
194 private static String decodeQuotedPrintable(String value) {
195 int length = value.length();
196 StringBuffer result = new StringBuffer(length);
197 for (int i = 0; i < length; i++) {
198 char c = value.charAt(i);
204 if (i < length - 2) {
205 char nextChar = value.charAt(i+1);
206 if (nextChar == '\r' || nextChar == '\n') {
207 // Ignore, it's just a continuation symbol
209 char nextNextChar = value.charAt(i+2);
211 int encodedChar = 16 * toHexValue(nextChar) + toHexValue(nextNextChar);
212 result.append((char) encodedChar);
213 } catch (IllegalArgumentException iae) {
214 // continue, assume it was incorrectly encoded
224 return result.toString();
227 private static int toHexValue(char c) {
228 if (c >= '0' && c <= '9') {
230 } else if (c >= 'A' && c <= 'F') {
232 } else if (c >= 'a' && c <= 'f') {
235 throw new IllegalArgumentException();
238 static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) {
239 String[] values = matchVCardPrefixedField(prefix, rawText, trim);
240 return values == null ? null : values[0];
243 private static boolean isLikeVCardDate(String value) {
247 // Not really sure this is true but matches practice
249 if (isStringOfDigits(value, 8)) {
254 value.length() == 10 &&
255 value.charAt(4) == '-' &&
256 value.charAt(7) == '-' &&
257 isSubstringOfDigits(value, 0, 4) &&
258 isSubstringOfDigits(value, 5, 2) &&
259 isSubstringOfDigits(value, 8, 2);
262 private static String formatAddress(String address) {
263 if (address == null) {
266 int length = address.length();
267 StringBuffer newAddress = new StringBuffer(length);
268 for (int j = 0; j < length; j++) {
269 char c = address.charAt(j);
271 newAddress.append(' ');
273 newAddress.append(c);
276 return newAddress.toString().trim();
280 * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like
281 * "Reverend John Q. Public III".
283 * @param names name values to format, in place
285 private static void formatNames(String[] names) {
287 for (int i = 0; i < names.length; i++) {
288 String name = names[i];
289 String[] components = new String[5];
292 int componentIndex = 0;
293 while ((end = name.indexOf(';', start)) > 0) {
294 components[componentIndex] = name.substring(start, end);
298 components[componentIndex] = name.substring(start);
299 StringBuffer newName = new StringBuffer(100);
300 maybeAppendComponent(components, 3, newName);
301 maybeAppendComponent(components, 1, newName);
302 maybeAppendComponent(components, 2, newName);
303 maybeAppendComponent(components, 0, newName);
304 maybeAppendComponent(components, 4, newName);
305 names[i] = newName.toString().trim();
310 private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) {
311 if (components[i] != null) {
313 newName.append(components[i]);