4d53971396d504300320fa82b78a127930016319
[zxing.git] / core / src / com / google / zxing / client / result / VCardResultParser.java
1 /*
2  * Copyright 2008 ZXing authors
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.google.zxing.client.result;
18
19 import com.google.zxing.Result;
20
21 import java.io.ByteArrayOutputStream;
22 import java.io.UnsupportedEncodingException;
23 import java.util.Vector;
24
25 /**
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.
28  *
29  * @author Sean Owen
30  */
31 final class VCardResultParser extends ResultParser {
32
33   private VCardResultParser() {
34   }
35
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")) {
42       return null;
43     }
44     String[] names = matchVCardPrefixedField("FN", rawText, true);
45     if (names == null) {
46       // If no display names found, look for regular name fields and format them
47       names = matchVCardPrefixedField("N", rawText, true);
48       formatNames(names);
49     }
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]);
57       }
58     }
59     String org = matchSingleVCardPrefixedField("ORG", rawText, true);
60     String birthday = matchSingleVCardPrefixedField("BDAY", rawText, true);
61     if (!isLikeVCardDate(birthday)) {
62       birthday = null;
63     }
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);
68   }
69
70   private static String[] matchVCardPrefixedField(String prefix, String rawText, boolean trim) {
71     Vector matches = null;
72     int i = 0;
73     int max = rawText.length();
74
75     while (i < max) {
76
77       i = rawText.indexOf(prefix, i);
78       if (i < 0) {
79         break;
80       }
81
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
84         i++;
85         continue;
86       }
87       i += prefix.length(); // Skip past this prefix we found to start
88       if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') {
89         continue;
90       }
91
92       int metadataStart = i;
93       while (rawText.charAt(i) != ':') { // Skip until a colon
94         i++;
95       }
96
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;
102         while (j <= i) {
103           if (rawText.charAt(j) == ';' || rawText.charAt(j) == ':') {
104             String metadata = rawText.substring(metadataStart+1, j);
105             int equals = metadata.indexOf('=');
106             if (equals >= 0) {
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;
112                 }
113               } else if (key.equalsIgnoreCase("CHARSET")) {
114                 quotedPrintableCharset = value;
115               }
116             }
117             metadataStart = j;
118           }
119           j++;
120         }
121       }
122
123       i++; // skip colon
124
125       int matchStart = i; // Found the start of a match here
126
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) == '=')) {
135           i++; // Skip \n
136         } else {
137           break;
138         }
139       }
140
141       if (i < 0) {
142         // No terminating end character? uh, done. Set i such that loop terminates and break
143         i = max;
144       } else if (i > matchStart) {
145         // found a match
146         if (matches == null) {
147           matches = new Vector(1); // lazy init
148         }
149         if (rawText.charAt(i-1) == '\r') {
150           i--; // Back up over \r, which really should be there
151         }
152         String element = rawText.substring(matchStart, i);
153         if (trim) {
154           element = element.trim();
155         }
156         if (quotedPrintable) {
157           element = decodeQuotedPrintable(element, quotedPrintableCharset);
158         } else {
159           element = stripContinuationCRLF(element);
160         }
161         matches.addElement(element);
162         i++;
163       } else {
164         i++;
165       }
166
167     }
168
169     if (matches == null || matches.isEmpty()) {
170       return null;
171     }
172     return toStringArray(matches);
173   }
174
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++) {
180       if (lastWasLF) {
181         lastWasLF = false;
182         continue;
183       }
184       char c = value.charAt(i);
185       lastWasLF = false;
186       switch (c) {
187         case '\n':
188           lastWasLF = true;
189           break;
190         case '\r':
191           break;
192         default:
193           result.append(c);
194       }
195     }
196     return result.toString();
197   }
198
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);
205       switch (c) {
206         case '\r':
207         case '\n':
208           break;
209         case '=':
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
214             } else {
215               char nextNextChar = value.charAt(i+2);
216               try {
217                 int encodedByte = 16 * toHexValue(nextChar) + toHexValue(nextNextChar);
218                 fragmentBuffer.write(encodedByte);
219               } catch (IllegalArgumentException iae) {
220                 // continue, assume it was incorrectly encoded
221               }
222               i += 2;
223             }
224           }
225           break;
226         default:
227           maybeAppendFragment(fragmentBuffer, charset, result);
228           result.append(c);
229       }
230     }
231     maybeAppendFragment(fragmentBuffer, charset, result);
232     return result.toString();
233   }
234
235   private static int toHexValue(char c) {
236     if (c >= '0' && c <= '9') {
237       return c - '0';
238     } else if (c >= 'A' && c <= 'F') {
239       return c - 'A' + 10;
240     } else if (c >= 'a' && c <= 'f') {
241       return c - 'a' + 10;
242     }
243     throw new IllegalArgumentException();
244   }
245
246   private static void maybeAppendFragment(ByteArrayOutputStream fragmentBuffer,
247                                           String charset,
248                                           StringBuffer result) {
249     if (fragmentBuffer.size() > 0) {
250       byte[] fragmentBytes = fragmentBuffer.toByteArray();
251       String fragment;
252       if (charset == null) {
253         fragment = new String(fragmentBytes);
254       } else {
255         try {
256           fragment = new String(fragmentBytes, charset);
257         } catch (UnsupportedEncodingException e) {
258           // Yikes, well try anyway:
259           fragment = new String(fragmentBytes);
260         }
261       }
262       fragmentBuffer.reset();
263       result.append(fragment);
264     }
265   }
266
267   static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) {
268     String[] values = matchVCardPrefixedField(prefix, rawText, trim);
269     return values == null ? null : values[0];
270   }
271
272   private static boolean isLikeVCardDate(String value) {
273     if (value == null) {
274       return true;
275     }
276     // Not really sure this is true but matches practice
277     // Mach YYYYMMDD
278     if (isStringOfDigits(value, 8)) {
279       return true;
280     }
281     // or YYYY-MM-DD
282     return
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);
289   }
290
291   private static String formatAddress(String address) {
292     if (address == null) {
293       return null;
294     }
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);
299       if (c == ';') {
300         newAddress.append(' ');
301       } else {
302         newAddress.append(c);
303       }
304     }
305     return newAddress.toString().trim();
306   }
307
308   /**
309    * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like
310    * "Reverend John Q. Public III".
311    *
312    * @param names name values to format, in place
313    */
314   private static void formatNames(String[] names) {
315     if (names != null) {
316       for (int i = 0; i < names.length; i++) {
317         String name = names[i];
318         String[] components = new String[5];
319         int start = 0;
320         int end;
321         int componentIndex = 0;
322         while ((end = name.indexOf(';', start)) > 0) {
323           components[componentIndex] = name.substring(start, end);
324           componentIndex++;
325           start = end + 1;
326         }
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();
335       }
336     }
337   }
338
339   private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) {
340     if (components[i] != null) {
341       newName.append(' ');
342       newName.append(components[i]);
343     }
344   }
345
346 }