Issue 309, don't fail if birthday is bad
[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     while (i < max) {
73       i = rawText.indexOf(prefix, i);
74       if (i < 0) {
75         break;
76       }
77       if (i > 0 && rawText.charAt(i - 1) != '\n') {
78         // then this didn't start a new token, we matched in the middle of something
79         i++;
80         continue;
81       }
82       i += prefix.length(); // Skip past this prefix we found to start
83       if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') {
84         continue;
85       }
86       while (rawText.charAt(i) != ':') { // Skip until a colon
87         i++;
88       }
89       i++; // skip colon
90       int start = i; // Found the start of a match here
91       i = rawText.indexOf((int) '\n', i); // Really, ends in \r\n
92       if (i < 0) {
93         // No terminating end character? uh, done. Set i such that loop terminates and break
94         i = max;
95       } else if (i > start) {
96         // found a match
97         if (matches == null) {
98           matches = new Vector(3); // lazy init
99         }
100         String element = rawText.substring(start, i);
101         if (trim) {
102           element = element.trim();
103         }
104         matches.addElement(element);
105         i++;
106       } else {
107         i++;
108       }
109     }
110     if (matches == null || matches.isEmpty()) {
111       return null;
112     }
113     return toStringArray(matches);
114   }
115
116   static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) {
117     String[] values = matchVCardPrefixedField(prefix, rawText, trim);
118     return values == null ? null : values[0];
119   }
120
121   private static boolean isLikeVCardDate(String value) {
122     if (value == null) {
123       return true;
124     }
125     // Not really sure this is true but matches practice
126     // Mach YYYYMMDD
127     if (isStringOfDigits(value, 8)) {
128       return true;
129     }
130     // or YYYY-MM-DD
131     return
132         value.length() == 10 &&
133         value.charAt(4) == '-' &&
134         value.charAt(7) == '-' &&
135         isSubstringOfDigits(value, 0, 4) &&
136         isSubstringOfDigits(value, 5, 2) &&
137         isSubstringOfDigits(value, 8, 2);
138   }
139
140   private static String formatAddress(String address) {
141     if (address == null) {
142       return null;
143     }
144     int length = address.length();
145     StringBuffer newAddress = new StringBuffer(length);
146     for (int j = 0; j < length; j++) {
147       char c = address.charAt(j);
148       if (c == ';') {
149         newAddress.append(' ');
150       } else {
151         newAddress.append(c);
152       }
153     }
154     return newAddress.toString().trim();
155   }
156
157   /**
158    * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like
159    * "Reverend John Q. Public III".
160    *
161    * @param names name values to format, in place
162    */
163   private static void formatNames(String[] names) {
164     if (names != null) {
165       for (int i = 0; i < names.length; i++) {
166         String name = names[i];
167         String[] components = new String[5];
168         int start = 0;
169         int end;
170         int componentIndex = 0;
171         while ((end = name.indexOf(';', start)) > 0) {
172           components[componentIndex] = name.substring(start, end);
173           componentIndex++;
174           start = end + 1;
175         }
176         components[componentIndex] = name.substring(start);
177         StringBuffer newName = new StringBuffer(100);
178         maybeAppendComponent(components, 3, newName);
179         maybeAppendComponent(components, 1, newName);
180         maybeAppendComponent(components, 2, newName);
181         maybeAppendComponent(components, 0, newName);
182         maybeAppendComponent(components, 4, newName);
183         names[i] = newName.toString().trim();
184       }
185     }
186   }
187
188   private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) {
189     if (components[i] != null) {
190       newName.append(' ');
191       newName.append(components[i]);
192     }
193   }
194
195 }