Add ENCODING=QUOTED-PRINTABLE support
[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.util.Vector;
22
23 /**
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.
26  *
27  * @author Sean Owen
28  */
29 final class VCardResultParser extends ResultParser {
30
31   private VCardResultParser() {
32   }
33
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")) {
40       return null;
41     }
42     String[] names = matchVCardPrefixedField("FN", rawText, true);
43     if (names == null) {
44       // If no display names found, look for regular name fields and format them
45       names = matchVCardPrefixedField("N", rawText, true);
46       formatNames(names);
47     }
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]);
55       }
56     }
57     String org = matchSingleVCardPrefixedField("ORG", rawText, true);
58     String birthday = matchSingleVCardPrefixedField("BDAY", rawText, true);
59     if (!isLikeVCardDate(birthday)) {
60       birthday = null;
61     }
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);
66   }
67
68   private static String[] matchVCardPrefixedField(String prefix, String rawText, boolean trim) {
69     Vector matches = null;
70     int i = 0;
71     int max = rawText.length();
72
73     while (i < max) {
74
75       i = rawText.indexOf(prefix, i);
76       if (i < 0) {
77         break;
78       }
79
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
82         i++;
83         continue;
84       }
85       i += prefix.length(); // Skip past this prefix we found to start
86       if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') {
87         continue;
88       }
89
90       int metadataStart = i;
91       while (rawText.charAt(i) != ':') { // Skip until a colon
92         i++;
93       }
94
95       boolean quotedPrintable = false;
96       if (i > metadataStart) {
97         // There was something after the tag, before colon
98         int j = metadataStart+1;
99         while (j <= i) {
100           if (rawText.charAt(j) == ';' || rawText.charAt(j) == ':') {
101             String metadata = rawText.substring(metadataStart+1, j);
102             int equals = metadata.indexOf('=');
103             if (equals >= 0) {
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;
109                 }
110               }
111             }
112             metadataStart = j;
113           }
114           j++;
115         }
116       }
117
118       i++; // skip colon
119
120       int matchStart = i; // Found the start of a match here
121
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) == '=')) {
130           i++; // Skip \n
131         } else {
132           break;
133         }
134       }
135
136       if (i < 0) {
137         // No terminating end character? uh, done. Set i such that loop terminates and break
138         i = max;
139       } else if (i > matchStart) {
140         // found a match
141         if (matches == null) {
142           matches = new Vector(1); // lazy init
143         }
144         if (rawText.charAt(i-1) == '\r') {
145           i--; // Back up over \r, which really should be there
146         }
147         String element = rawText.substring(matchStart, i);
148         if (trim) {
149           element = element.trim();
150         }
151         if (quotedPrintable) {
152           element = decodeQuotedPrintable(element);
153         } else {
154           element = stripContinuationCRLF(element);
155         }
156         matches.addElement(element);
157         i++;
158       } else {
159         i++;
160       }
161
162     }
163
164     if (matches == null || matches.isEmpty()) {
165       return null;
166     }
167     return toStringArray(matches);
168   }
169
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++) {
175       if (lastWasLF) {
176         lastWasLF = false;
177         continue;
178       }
179       char c = value.charAt(i);
180       lastWasLF = false;
181       switch (c) {
182         case '\n':
183           lastWasLF = true;
184           break;
185         case '\r':
186           break;
187         default:
188           result.append(c);
189       }
190     }
191     return result.toString();
192   }
193
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);
199       switch (c) {
200         case '\r':
201         case '\n':
202           break;
203         case '=':
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
208             } else {
209               char nextNextChar = value.charAt(i+2);
210               try {
211                 int encodedChar = 16 * toHexValue(nextChar) + toHexValue(nextNextChar);
212                 result.append((char) encodedChar);
213               } catch (IllegalArgumentException iae) {
214                 // continue, assume it was incorrectly encoded
215               }
216               i += 2;
217             }
218           }
219           break;
220         default:
221           result.append(c);
222       }
223     }
224     return result.toString();
225   }
226
227   private static int toHexValue(char c) {
228     if (c >= '0' && c <= '9') {
229       return c - '0';
230     } else if (c >= 'A' && c <= 'F') {
231       return c - 'A' + 10;
232     } else if (c >= 'a' && c <= 'f') {
233       return c - 'a' + 10;
234     }
235     throw new IllegalArgumentException();
236   }
237
238   static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) {
239     String[] values = matchVCardPrefixedField(prefix, rawText, trim);
240     return values == null ? null : values[0];
241   }
242
243   private static boolean isLikeVCardDate(String value) {
244     if (value == null) {
245       return true;
246     }
247     // Not really sure this is true but matches practice
248     // Mach YYYYMMDD
249     if (isStringOfDigits(value, 8)) {
250       return true;
251     }
252     // or YYYY-MM-DD
253     return
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);
260   }
261
262   private static String formatAddress(String address) {
263     if (address == null) {
264       return null;
265     }
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);
270       if (c == ';') {
271         newAddress.append(' ');
272       } else {
273         newAddress.append(c);
274       }
275     }
276     return newAddress.toString().trim();
277   }
278
279   /**
280    * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like
281    * "Reverend John Q. Public III".
282    *
283    * @param names name values to format, in place
284    */
285   private static void formatNames(String[] names) {
286     if (names != null) {
287       for (int i = 0; i < names.length; i++) {
288         String name = names[i];
289         String[] components = new String[5];
290         int start = 0;
291         int end;
292         int componentIndex = 0;
293         while ((end = name.indexOf(';', start)) > 0) {
294           components[componentIndex] = name.substring(start, end);
295           componentIndex++;
296           start = end + 1;
297         }
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();
306       }
307     }
308   }
309
310   private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) {
311     if (components[i] != null) {
312       newName.append(' ');
313       newName.append(components[i]);
314     }
315   }
316
317 }