001 package ezvcard.io;
002
003 import java.io.Closeable;
004 import java.io.File;
005 import java.io.FileNotFoundException;
006 import java.io.FileReader;
007 import java.io.IOException;
008 import java.io.InputStream;
009 import java.io.InputStreamReader;
010 import java.io.Reader;
011 import java.io.StringReader;
012 import java.lang.reflect.Method;
013 import java.util.ArrayList;
014 import java.util.Arrays;
015 import java.util.HashMap;
016 import java.util.List;
017 import java.util.Map;
018
019 import org.apache.commons.codec.DecoderException;
020 import org.apache.commons.codec.net.QuotedPrintableCodec;
021
022 import ezvcard.VCard;
023 import ezvcard.VCardSubTypes;
024 import ezvcard.VCardVersion;
025 import ezvcard.parameters.EncodingParameter;
026 import ezvcard.parameters.TypeParameter;
027 import ezvcard.parameters.ValueParameter;
028 import ezvcard.types.AddressType;
029 import ezvcard.types.LabelType;
030 import ezvcard.types.RawType;
031 import ezvcard.types.TypeList;
032 import ezvcard.types.VCardType;
033 import ezvcard.util.VCardStringUtils;
034
035 /*
036 Copyright (c) 2012, Michael Angstadt
037 All rights reserved.
038
039 Redistribution and use in source and binary forms, with or without
040 modification, are permitted provided that the following conditions are met:
041
042 1. Redistributions of source code must retain the above copyright notice, this
043 list of conditions and the following disclaimer.
044 2. Redistributions in binary form must reproduce the above copyright notice,
045 this list of conditions and the following disclaimer in the documentation
046 and/or other materials provided with the distribution.
047
048 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
049 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
050 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
051 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
052 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
053 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
054 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
055 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
056 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
057 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
058
059 The views and conclusions contained in the software and documentation are those
060 of the authors and should not be interpreted as representing official policies,
061 either expressed or implied, of the FreeBSD Project.
062 */
063
064 /**
065 * Unmarshals vCards into {@link VCard} objects.
066 * @author Michael Angstadt
067 */
068 public class VCardReader implements Closeable, IParser {
069 private CompatibilityMode compatibilityMode = CompatibilityMode.RFC;
070 private List<String> warnings = new ArrayList<String>();
071 private Map<String, Class<? extends VCardType>> extendedTypeClasses = new HashMap<String, Class<? extends VCardType>>();
072 private FoldedLineReader reader;
073 private boolean caretDecodingEnabled = true;
074
075 /**
076 * @param str the string to read the vCards from
077 */
078 public VCardReader(String str) {
079 this(new StringReader(str));
080 }
081
082 /**
083 * @param in the input stream to read the vCards from
084 */
085 public VCardReader(InputStream in) {
086 this(new InputStreamReader(in));
087 }
088
089 /**
090 * @param file the file to read the vCards from
091 * @throws FileNotFoundException if the file doesn't exist
092 */
093 public VCardReader(File file) throws FileNotFoundException {
094 this(new FileReader(file));
095 }
096
097 /**
098 * @param reader the reader to read the vCards from
099 */
100 public VCardReader(Reader reader) {
101 this.reader = new FoldedLineReader(reader);
102 }
103
104 /**
105 * This constructor is used for reading embedded 2.1 vCards (see 2.1 docs,
106 * p.19)
107 * @param reader the reader to read the vCards from
108 */
109 private VCardReader(FoldedLineReader reader) {
110 this.reader = reader;
111 }
112
113 /**
114 * <p>
115 * Gets whether the reader will decode characters in parameter values that
116 * use circumflex accent encoding. This escaping mechanism allows for
117 * newlines and double quotes to be included in parameter values.
118 * </p>
119 *
120 * <table border="1">
121 * <tr>
122 * <th>Raw</th>
123 * <th>Encoded</th>
124 * </tr>
125 * <tr>
126 * <td><code>"</code></td>
127 * <td><code>^'</code></td>
128 * </tr>
129 * <tr>
130 * <td><i>newline</i></td>
131 * <td><code>^n</code></td>
132 * </tr>
133 * <tr>
134 * <td><code>^</code></td>
135 * <td><code>^^</code></td>
136 * </tr>
137 * </table>
138 *
139 * <p>
140 * This setting is enabled by default and is only used with 3.0 and 4.0
141 * vCards.
142 * </p>
143 *
144 * <p>
145 * Example:
146 * </p>
147 *
148 * <pre>
149 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
150 * sburgh, PA 15212":geo:40.446816,-80.00566
151 * </pre>
152 *
153 * @return true if circumflex accent decoding is enabled, false if not
154 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
155 */
156 public boolean isCaretDecodingEnabled() {
157 return caretDecodingEnabled;
158 }
159
160 /**
161 * <p>
162 * Sets whether the reader will decode characters in parameter values that
163 * use circumflex accent encoding. This escaping mechanism allows for
164 * newlines and double quotes to be included in parameter values.
165 * </p>
166 *
167 * <table border="1">
168 * <tr>
169 * <th>Raw</th>
170 * <th>Encoded</th>
171 * </tr>
172 * <tr>
173 * <td><code>"</code></td>
174 * <td><code>^'</code></td>
175 * </tr>
176 * <tr>
177 * <td><i>newline</i></td>
178 * <td><code>^n</code></td>
179 * </tr>
180 * <tr>
181 * <td><code>^</code></td>
182 * <td><code>^^</code></td>
183 * </tr>
184 * </table>
185 *
186 * <p>
187 * This setting is enabled by default and is only used with 3.0 and 4.0
188 * vCards.
189 * </p>
190 *
191 * <p>
192 * Example:
193 * </p>
194 *
195 * <pre>
196 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
197 * sburgh, PA 15212":geo:40.446816,-80.00566
198 * </pre>
199 *
200 * @param enable true to use circumflex accent decoding, false not to
201 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
202 */
203 public void setCaretDecodingEnabled(boolean enable) {
204 caretDecodingEnabled = enable;
205 }
206
207 /**
208 * Gets the compatibility mode. Used for customizing the unmarshalling
209 * process based on the application that generated the vCard.
210 * @return the compatibility mode
211 */
212 @Deprecated
213 public CompatibilityMode getCompatibilityMode() {
214 return compatibilityMode;
215 }
216
217 /**
218 * Sets the compatibility mode. Used for customizing the unmarshalling
219 * process based on the application that generated the vCard.
220 * @param compatibilityMode the compatibility mode
221 */
222 @Deprecated
223 public void setCompatibilityMode(CompatibilityMode compatibilityMode) {
224 this.compatibilityMode = compatibilityMode;
225 }
226
227 //@Override
228 public void registerExtendedType(Class<? extends VCardType> clazz) {
229 extendedTypeClasses.put(getTypeNameFromTypeClass(clazz), clazz);
230 }
231
232 //@Override
233 public void unregisterExtendedType(Class<? extends VCardType> clazz) {
234 extendedTypeClasses.remove(getTypeNameFromTypeClass(clazz));
235 }
236
237 //@Override
238 public List<String> getWarnings() {
239 return new ArrayList<String>(warnings);
240 }
241
242 //@Override
243 public VCard readNext() throws IOException {
244 warnings.clear();
245
246 VCard vcard = new VCard();
247
248 List<LabelType> labels = new ArrayList<LabelType>();
249 VCardVersion version = null;
250 boolean endFound = false;
251 int typesRead = 0;
252 String line;
253 List<String> warningsBuf = new ArrayList<String>();
254
255 while (!endFound && (line = reader.readLine()) != null) {
256 //parse the components out of the line
257 VCardLine parsedLine = VCardLine.parse(line, version, caretDecodingEnabled);
258 if (parsedLine == null) {
259 warnings.add("Skipping malformed vCard line: \"" + line + "\"");
260 continue;
261 }
262
263 //build the sub types
264 VCardSubTypes subTypes = new VCardSubTypes();
265 for (List<String> subType : parsedLine.getSubTypes()) {
266 //if the parameter is name-less, make a guess at what the name is
267 //v3.0 and v4.0 requires all sub types to have names, but v2.1 does not
268 String subTypeName = subType.get(0);
269 List<String> subTypeValues = subType.subList(1, subType.size());
270 if (subTypeName == null) {
271 String subTypeValue = subTypeValues.get(0);
272 if (ValueParameter.valueOf(subTypeValue) != null) {
273 subTypeName = ValueParameter.NAME;
274 } else if (EncodingParameter.valueOf(subTypeValue) != null) {
275 subTypeName = EncodingParameter.NAME;
276 } else {
277 //otherwise, assume it's a TYPE
278 subTypeName = TypeParameter.NAME;
279 }
280
281 subTypes.put(subTypeName, subTypeValue);
282 } else {
283 subTypeName = subTypeName.toUpperCase();
284
285 //account for multi-valued TYPE parameters being enclosed entirely in double quotes
286 //e.g. ADR;TYPE="home,work"
287 if (subTypeName.equals(TypeParameter.NAME) && subTypeValues.size() == 1) {
288 //@formatter:off
289 /*
290 * Many examples throughout the 4.0 specs show TYPE parameters being encoded in this way.
291 * This conflicts with the ABNF and is noted in the errata.
292 * Split the value by comma incase the vendor implemented it this way.
293 */
294 //@formatter:on
295 subTypeValues = Arrays.asList(subTypeValues.get(0).split(","));
296 }
297
298 for (String subTypeValue : subTypeValues) {
299 subTypes.put(subTypeName, subTypeValue);
300 }
301 }
302 }
303
304 String typeName = parsedLine.getTypeName().toUpperCase();
305 String value = VCardStringUtils.ltrim(parsedLine.getValue());
306 String groupName = parsedLine.getGroup();
307
308 //if the value is encoded in "quoted-printable", decode it
309 //"quoted-printable" encoding is only supported in v2.1
310 if (subTypes.getEncoding() == EncodingParameter.QUOTED_PRINTABLE) {
311 QuotedPrintableCodec codec = new QuotedPrintableCodec();
312 String charset = subTypes.getCharset();
313 try {
314 value = (charset == null) ? codec.decode(value) : codec.decode(value, charset);
315 } catch (DecoderException e) {
316 warnings.add("The value of the " + typeName + " type was marked as \"quoted-printable\", but it could not be decoded. Assuming that the value is plain text.");
317 }
318 subTypes.setEncoding(null); //remove encoding sub type
319 }
320
321 //vCard should start with "BEGIN"
322 if (typesRead == 0 && !"BEGIN".equals(typeName)) {
323 warnings.add("vCard does not start with \"BEGIN\".");
324 }
325
326 typesRead++;
327
328 if ("BEGIN".equals(typeName)) {
329 if (!"vcard".equalsIgnoreCase(value)) {
330 warnings.add("The value of the BEGIN property should be \"vcard\", but it is \"" + value + "\".");
331 }
332 } else if ("END".equals(typeName)) {
333 endFound = true;
334 if (!"vcard".equalsIgnoreCase(value)) {
335 warnings.add("The value of the END property should be \"vcard\", but it is \"" + value + "\".");
336 }
337 } else if ("VERSION".equals(typeName)) {
338 if (version == null) {
339 version = VCardVersion.valueOfByStr(value);
340 if (version == null) {
341 warnings.add("Invalid value of VERSION property: " + value);
342 }
343 } else {
344 warnings.add("Additional VERSION property encountered: \"" + value + "\". It will be ignored.");
345 }
346 vcard.setVersion(version);
347 } else {
348 //create the type object
349 VCardType type = createTypeObject(typeName);
350 type.setGroup(groupName);
351
352 //unmarshal the text string into the object
353 warningsBuf.clear();
354 try {
355 type.unmarshalText(subTypes, value, version, warningsBuf, compatibilityMode);
356
357 //add to vcard
358 if (type instanceof LabelType) {
359 //LABELs must be treated specially so they can be matched up with their ADRs
360 labels.add((LabelType) type);
361 } else {
362 addToVCard(type, vcard);
363 }
364 } catch (SkipMeException e) {
365 warningsBuf.add(type.getTypeName() + " property will not be unmarshalled: " + e.getMessage());
366 } catch (EmbeddedVCardException e) {
367 //parse an embedded vCard (i.e. the AGENT type)
368
369 VCardReader agentReader;
370 if (value.length() == 0 || version == null || version == VCardVersion.V2_1) {
371 //vCard will be added as a nested vCard (2.1 style)
372 agentReader = new VCardReader(reader);
373 } else {
374 //vCard will be contained within the type value (3.0 style)
375 value = VCardStringUtils.unescape(value);
376 agentReader = new VCardReader(new StringReader(value));
377 }
378
379 agentReader.setCompatibilityMode(compatibilityMode);
380 try {
381 VCard agentVcard = agentReader.readNext();
382 e.injectVCard(agentVcard);
383 } finally {
384 for (String w : agentReader.getWarnings()) {
385 warnings.add("Problem unmarshalling nested vCard value from " + type.getTypeName() + ": " + w);
386 }
387 }
388
389 addToVCard(type, vcard);
390 } finally {
391 warnings.addAll(warningsBuf);
392 }
393 }
394 }
395
396 if (typesRead == 0) {
397 //end of stream reached
398 return null;
399 }
400
401 //assign labels to their addresses
402 for (LabelType label : labels) {
403 boolean orphaned = true;
404 for (AddressType adr : vcard.getAddresses()) {
405 if (adr.getLabel() == null && adr.getTypes().equals(label.getTypes())) {
406 adr.setLabel(label.getValue());
407 orphaned = false;
408 break;
409 }
410 }
411 if (orphaned) {
412 vcard.addOrphanedLabel(label);
413 }
414 }
415
416 if (!endFound) {
417 warnings.add("vCard does not terminate with the END property.");
418 }
419
420 return vcard;
421 }
422
423 /**
424 * Creates the appropriate {@link VCardType} instance, given the type name.
425 * This method does not unmarshal the type, it just creates the type object.
426 * @param name the type name (e.g. "FN")
427 * @return the Type that was created
428 */
429 private VCardType createTypeObject(String name) {
430 Class<? extends VCardType> clazz = TypeList.getTypeClass(name);
431 VCardType t;
432 if (clazz != null) {
433 try {
434 //create a new instance of the class
435 t = clazz.newInstance();
436 } catch (Exception e) {
437 //it is the responsibility of the EZ-vCard developer to ensure that this exception is never thrown
438 //all type classes defined in the EZ-vCard library MUST have public, no-arg constructors
439 throw new RuntimeException(e);
440 }
441 } else {
442 Class<? extends VCardType> extendedTypeClass = extendedTypeClasses.get(name);
443 if (extendedTypeClass != null) {
444 try {
445 t = extendedTypeClass.newInstance();
446 } catch (Exception e) {
447 //this should never happen because the type class is checked to see if it has a public, no-arg constructor in the "registerExtendedType" method
448 throw new RuntimeException("Extended type class \"" + extendedTypeClass.getName() + "\" must have a public, no-arg constructor.");
449 }
450 } else {
451 t = new RawType(name); //use RawType instead of TextType because we don't want to unescape any characters that might be meaningful to this type
452 if (!name.startsWith("X-")) {
453 warnings.add("Non-standard type \"" + name + "\" found. Treating it as an extended type.");
454 }
455 }
456 }
457 return t;
458 }
459
460 /**
461 * Adds a type object to the vCard.
462 * @param t the type object
463 * @param vcard the vCard
464 */
465 private void addToVCard(VCardType t, VCard vcard) {
466 Method method = TypeList.getAddMethod(t.getClass());
467 if (method != null) {
468 try {
469 method.invoke(vcard, t);
470 } catch (Exception e) {
471 //this should NEVER be thrown because the method MUST be public
472 throw new RuntimeException(e);
473 }
474 } else {
475 vcard.addExtendedType(t);
476 }
477 }
478
479 /**
480 * Gets the type name from a type class.
481 * @param clazz the type class
482 * @return the type name
483 */
484 private String getTypeNameFromTypeClass(Class<? extends VCardType> clazz) {
485 try {
486 VCardType t = clazz.newInstance();
487 return t.getTypeName().toUpperCase();
488 } catch (Exception e) {
489 //there is no public, no-arg constructor
490 throw new RuntimeException(e);
491 }
492 }
493
494 /**
495 * Closes the underlying {@link Reader} object.
496 */
497 public void close() throws IOException {
498 reader.close();
499 }
500 }