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.io.ByteArrayOutputStream;
22 import java.io.UnsupportedEncodingException;
23 import java.util.Vector;
26 * Parses contact information formatted according to the VCard (2.1) format. This is not a complete
27 * implementation but should parse information as commonly encoded in 2D barcodes.
31 final class VCardResultParser extends ResultParser {
33 private VCardResultParser() {
36 public static AddressBookParsedResult parse(Result result) {
37 // Although we should insist on the raw text ending with "END:VCARD", there's no reason
38 // to throw out everything else we parsed just because this was omitted. In fact, Eclair
39 // is doing just that, and we can't parse its contacts without this leniency.
40 String rawText = result.getText();
41 if (rawText == null || !rawText.startsWith("BEGIN:VCARD")) {
44 String[] names = matchVCardPrefixedField("FN", rawText, true);
46 // If no display names found, look for regular name fields and format them
47 names = matchVCardPrefixedField("N", rawText, true);
50 String[] phoneNumbers = matchVCardPrefixedField("TEL", rawText, true);
51 String[] emails = matchVCardPrefixedField("EMAIL", rawText, true);
52 String note = matchSingleVCardPrefixedField("NOTE", rawText, false);
53 String[] addresses = matchVCardPrefixedField("ADR", rawText, true);
54 if (addresses != null) {
55 for (int i = 0; i < addresses.length; i++) {
56 addresses[i] = formatAddress(addresses[i]);
59 String org = matchSingleVCardPrefixedField("ORG", rawText, true);
60 String birthday = matchSingleVCardPrefixedField("BDAY", rawText, true);
61 if (!isLikeVCardDate(birthday)) {
64 String title = matchSingleVCardPrefixedField("TITLE", rawText, true);
65 String url = matchSingleVCardPrefixedField("URL", rawText, true);
66 return new AddressBookParsedResult(names, null, phoneNumbers, emails, note, addresses, org,
67 birthday, title, url);
70 private static String[] matchVCardPrefixedField(String prefix, String rawText, boolean trim) {
71 Vector matches = null;
73 int max = rawText.length();
77 i = rawText.indexOf(prefix, i);
82 if (i > 0 && rawText.charAt(i - 1) != '\n') {
83 // then this didn't start a new token, we matched in the middle of something
87 i += prefix.length(); // Skip past this prefix we found to start
88 if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') {
92 int metadataStart = i;
93 while (rawText.charAt(i) != ':') { // Skip until a colon
97 boolean quotedPrintable = false;
98 String quotedPrintableCharset = null;
99 if (i > metadataStart) {
100 // There was something after the tag, before colon
101 int j = metadataStart+1;
103 if (rawText.charAt(j) == ';' || rawText.charAt(j) == ':') {
104 String metadata = rawText.substring(metadataStart+1, j);
105 int equals = metadata.indexOf('=');
107 String key = metadata.substring(0, equals);
108 String value = metadata.substring(equals+1);
109 if (key.equalsIgnoreCase("ENCODING")) {
110 if (value.equalsIgnoreCase("QUOTED-PRINTABLE")) {
111 quotedPrintable = true;
113 } else if (key.equalsIgnoreCase("CHARSET")) {
114 quotedPrintableCharset = value;
125 int matchStart = i; // Found the start of a match here
127 while ((i = rawText.indexOf((int) '\n', i)) >= 0) { // Really, end in \r\n
128 if (i < rawText.length() - 1 && // But if followed by tab or space,
129 (rawText.charAt(i+1) == ' ' || // this is only a continuation
130 rawText.charAt(i+1) == '\t')) {
131 i += 2; // Skip \n and continutation whitespace
132 } else if (quotedPrintable && // If preceded by = in quoted printable
133 (rawText.charAt(i-1) == '=' || // this is a continuation
134 rawText.charAt(i-2) == '=')) {
142 // No terminating end character? uh, done. Set i such that loop terminates and break
144 } else if (i > matchStart) {
146 if (matches == null) {
147 matches = new Vector(1); // lazy init
149 if (rawText.charAt(i-1) == '\r') {
150 i--; // Back up over \r, which really should be there
152 String element = rawText.substring(matchStart, i);
154 element = element.trim();
156 if (quotedPrintable) {
157 element = decodeQuotedPrintable(element, quotedPrintableCharset);
159 element = stripContinuationCRLF(element);
161 matches.addElement(element);
169 if (matches == null || matches.isEmpty()) {
172 return toStringArray(matches);
175 private static String stripContinuationCRLF(String value) {
176 int length = value.length();
177 StringBuffer result = new StringBuffer(length);
178 boolean lastWasLF = false;
179 for (int i = 0; i < length; i++) {
184 char c = value.charAt(i);
196 return result.toString();
199 private static String decodeQuotedPrintable(String value, String charset) {
200 int length = value.length();
201 StringBuffer result = new StringBuffer(length);
202 ByteArrayOutputStream fragmentBuffer = new ByteArrayOutputStream();
203 for (int i = 0; i < length; i++) {
204 char c = value.charAt(i);
210 if (i < length - 2) {
211 char nextChar = value.charAt(i+1);
212 if (nextChar == '\r' || nextChar == '\n') {
213 // Ignore, it's just a continuation symbol
215 char nextNextChar = value.charAt(i+2);
217 int encodedByte = 16 * toHexValue(nextChar) + toHexValue(nextNextChar);
218 fragmentBuffer.write(encodedByte);
219 } catch (IllegalArgumentException iae) {
220 // continue, assume it was incorrectly encoded
227 maybeAppendFragment(fragmentBuffer, charset, result);
231 maybeAppendFragment(fragmentBuffer, charset, result);
232 return result.toString();
235 private static int toHexValue(char c) {
236 if (c >= '0' && c <= '9') {
238 } else if (c >= 'A' && c <= 'F') {
240 } else if (c >= 'a' && c <= 'f') {
243 throw new IllegalArgumentException();
246 private static void maybeAppendFragment(ByteArrayOutputStream fragmentBuffer,
248 StringBuffer result) {
249 if (fragmentBuffer.size() > 0) {
250 byte[] fragmentBytes = fragmentBuffer.toByteArray();
252 if (charset == null) {
253 fragment = new String(fragmentBytes);
256 fragment = new String(fragmentBytes, charset);
257 } catch (UnsupportedEncodingException e) {
258 // Yikes, well try anyway:
259 fragment = new String(fragmentBytes);
262 fragmentBuffer.reset();
263 result.append(fragment);
267 static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) {
268 String[] values = matchVCardPrefixedField(prefix, rawText, trim);
269 return values == null ? null : values[0];
272 private static boolean isLikeVCardDate(String value) {
276 // Not really sure this is true but matches practice
278 if (isStringOfDigits(value, 8)) {
283 value.length() == 10 &&
284 value.charAt(4) == '-' &&
285 value.charAt(7) == '-' &&
286 isSubstringOfDigits(value, 0, 4) &&
287 isSubstringOfDigits(value, 5, 2) &&
288 isSubstringOfDigits(value, 8, 2);
291 private static String formatAddress(String address) {
292 if (address == null) {
295 int length = address.length();
296 StringBuffer newAddress = new StringBuffer(length);
297 for (int j = 0; j < length; j++) {
298 char c = address.charAt(j);
300 newAddress.append(' ');
302 newAddress.append(c);
305 return newAddress.toString().trim();
309 * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like
310 * "Reverend John Q. Public III".
312 * @param names name values to format, in place
314 private static void formatNames(String[] names) {
316 for (int i = 0; i < names.length; i++) {
317 String name = names[i];
318 String[] components = new String[5];
321 int componentIndex = 0;
322 while ((end = name.indexOf(';', start)) > 0) {
323 components[componentIndex] = name.substring(start, end);
327 components[componentIndex] = name.substring(start);
328 StringBuffer newName = new StringBuffer(100);
329 maybeAppendComponent(components, 3, newName);
330 maybeAppendComponent(components, 1, newName);
331 maybeAppendComponent(components, 2, newName);
332 maybeAppendComponent(components, 0, newName);
333 maybeAppendComponent(components, 4, newName);
334 names[i] = newName.toString().trim();
339 private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) {
340 if (components[i] != null) {
342 newName.append(components[i]);