001 package ezvcard.io.scribe;
002
003 import static ezvcard.util.StringUtils.NEWLINE;
004 import static ezvcard.util.StringUtils.join;
005
006 import java.util.ArrayList;
007 import java.util.Arrays;
008 import java.util.Collection;
009 import java.util.Date;
010 import java.util.Iterator;
011 import java.util.List;
012 import java.util.regex.Pattern;
013
014 import javax.xml.namespace.QName;
015
016 import org.w3c.dom.Element;
017
018 import ezvcard.VCard;
019 import ezvcard.VCardDataType;
020 import ezvcard.VCardVersion;
021 import ezvcard.io.CannotParseException;
022 import ezvcard.io.EmbeddedVCardException;
023 import ezvcard.io.SkipMeException;
024 import ezvcard.io.html.HCardElement;
025 import ezvcard.io.json.JCardValue;
026 import ezvcard.io.text.VCardRawWriter;
027 import ezvcard.io.xml.XCardElement;
028 import ezvcard.parameter.VCardParameters;
029 import ezvcard.property.VCardProperty;
030 import ezvcard.util.ISOFormat;
031 import ezvcard.util.StringUtils.JoinCallback;
032 import ezvcard.util.VCardDateFormatter;
033 import ezvcard.util.XmlUtils;
034
035 /*
036 Copyright (c) 2013, 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
060 /**
061 * Base class for vCard property marshallers.
062 * @param <T> the property class
063 * @author Michael Angstadt
064 */
065 public abstract class VCardPropertyScribe<T extends VCardProperty> {
066 protected final Class<T> clazz;
067 protected final String propertyName;
068 protected final QName qname;
069
070 /**
071 * Creates a new marshaller.
072 * @param clazz the property class
073 * @param propertyName the property name (e.g. "FN")
074 */
075 public VCardPropertyScribe(Class<T> clazz, String propertyName) {
076 this(clazz, propertyName, new QName(VCardVersion.V4_0.getXmlNamespace(), propertyName.toLowerCase()));
077 }
078
079 /**
080 * Creates a new marshaller.
081 * @param clazz the property class
082 * @param propertyName the property name (e.g. "FN")
083 * @param qname the XML element name and namespace to use for xCard
084 * documents (by default, the XML element name is set to the lower-cased
085 * property name, and the element namespace is set to the xCard namespace)
086 */
087 public VCardPropertyScribe(Class<T> clazz, String propertyName, QName qname) {
088 this.clazz = clazz;
089 this.propertyName = propertyName;
090 this.qname = qname;
091 }
092
093 /**
094 * Gets the property class.
095 * @return the property class
096 */
097 public Class<T> getPropertyClass() {
098 return clazz;
099 }
100
101 /**
102 * Gets the property name.
103 * @return the property name (e.g. "FN")
104 */
105 public String getPropertyName() {
106 return propertyName;
107 }
108
109 /**
110 * Gets this property's local name and namespace for xCard documents.
111 * @return the XML local name and namespace
112 */
113 public QName getQName() {
114 return qname;
115 }
116
117 /**
118 * Sanitizes a property's parameters (called before the property is
119 * written). Note that a copy of the parameters is returned so that the
120 * property object does not get modified.
121 * @param property the property
122 * @param version the version of the vCard that is being generated
123 * @param vcard the vCard that the property belongs to
124 * @return the sanitized parameters
125 */
126 public final VCardParameters prepareParameters(T property, VCardVersion version, VCard vcard) {
127 //make a copy because the property should not get modified when it is marshalled
128 VCardParameters copy = new VCardParameters(property.getParameters());
129 _prepareParameters(property, copy, version, vcard);
130 return copy;
131 }
132
133 /**
134 * <p>
135 * Determines the property's default data type.
136 * </p>
137 * <p>
138 * When writing a plain-text vCard, if the data type of a property instance
139 * (as determined by the {@link #dataType} method) matches the default data
140 * type, then a VALUE parameter will *not* be written.
141 * </p>
142 * <p>
143 * When parsing a plain-text vCard, if a property has no VALUE parameter,
144 * then the property's default data type will be passed into the
145 * {@link #parseText} method.
146 * </p>
147 * @param version the vCard version
148 * @return the default data type or null if unknown
149 */
150 public final VCardDataType defaultDataType(VCardVersion version) {
151 return _defaultDataType(version);
152 }
153
154 /**
155 * Determines the data type of a property instance.
156 * @param property the property
157 * @param version the version of the vCard that is being generated
158 * @return the data type or null if unknown
159 */
160 public final VCardDataType dataType(T property, VCardVersion version) {
161 return _dataType(property, version);
162 }
163
164 /**
165 * Marshals a property's value to a string.
166 * @param property the property
167 * @param version the version of the vCard that is being generated
168 * @return the marshalled value
169 * @throws SkipMeException if the property should not be written to the data
170 * stream
171 */
172 public final String writeText(T property, VCardVersion version) {
173 return _writeText(property, version);
174 }
175
176 /**
177 * Marshals a property's value to an XML element (xCard).
178 * @param property the property
179 * @param element the property's XML element.
180 * @throws SkipMeException if the property should not be written to the data
181 * stream
182 */
183 public final void writeXml(T property, Element element) {
184 XCardElement xCardElement = new XCardElement(element);
185 _writeXml(property, xCardElement);
186 }
187
188 /**
189 * Marshals a property's value to a JSON data stream (jCard).
190 * @param property the property
191 * @return the marshalled value
192 * @throws SkipMeException if the property should not be written to the data
193 * stream
194 */
195 public final JCardValue writeJson(T property) {
196 return _writeJson(property);
197 }
198
199 /**
200 * Unmarshals a property from a plain-text vCard.
201 * @param value the value as read off the wire
202 * @param dataType the data type of the property value. The property's VALUE
203 * parameter is used to determine the data type. If the property has no
204 * VALUE parameter, then this parameter will be set to the property's
205 * default datatype, as determined by the {@link #defaultDataType} method.
206 * Note that the VALUE parameter is removed from the property's parameter
207 * list after it has been read.
208 * @param version the version of the vCard that is being parsed
209 * @param parameters the parsed parameters
210 * @return the unmarshalled property and its warnings
211 * @throws CannotParseException if the marshaller could not parse the
212 * property's value
213 * @throws SkipMeException if the property should not be added to the final
214 * {@link VCard} object
215 * @throws EmbeddedVCardException if the property value is an embedded vCard
216 * (i.e. the AGENT property)
217 */
218 public final Result<T> parseText(String value, VCardDataType dataType, VCardVersion version, VCardParameters parameters) {
219 List<String> warnings = new ArrayList<String>(0);
220 T property = _parseText(value, dataType, version, parameters, warnings);
221 property.setParameters(parameters);
222 return new Result<T>(property, warnings);
223 }
224
225 /**
226 * Unmarshals a property's value from an XML document (xCard).
227 * @param element the property's XML element
228 * @param parameters the parsed parameters
229 * @return the unmarshalled property and its warnings
230 * @throws CannotParseException if the marshaller could not parse the
231 * property's value
232 * @throws SkipMeException if the property should not be added to the final
233 * {@link VCard} object
234 */
235 public final Result<T> parseXml(Element element, VCardParameters parameters) {
236 List<String> warnings = new ArrayList<String>(0);
237 T property = _parseXml(new XCardElement(element), parameters, warnings);
238 property.setParameters(parameters);
239 return new Result<T>(property, warnings);
240 }
241
242 /**
243 * Unmarshals the property from an HTML document (hCard).
244 * @param element the property's HTML element
245 * @return the unmarshalled property and its warnings
246 * @throws CannotParseException if the property value could not be parsed
247 * @throws SkipMeException if this type should NOT be added to the
248 * {@link VCard} object
249 * @throws EmbeddedVCardException if the property value is an embedded vCard
250 * (i.e. the AGENT property)
251 */
252 public final Result<T> parseHtml(org.jsoup.nodes.Element element) {
253 HCardElement hcardElement = new HCardElement(element);
254 List<String> warnings = new ArrayList<String>(0);
255 T property = _parseHtml(hcardElement, warnings);
256 return new Result<T>(property, warnings);
257 }
258
259 /**
260 * Unmarshals a property's value from a JSON data stream (jCard).
261 * @param value the property's JSON value
262 * @param dataType the data type
263 * @param parameters the parsed parameters
264 * @return the unmarshalled property and its warnings
265 * @throws CannotParseException if the marshaller could not parse the
266 * property's value
267 * @throws SkipMeException if the property should not be added to the final
268 * {@link VCard} object
269 */
270 public final Result<T> parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters) {
271 List<String> warnings = new ArrayList<String>(0);
272 T property = _parseJson(value, dataType, parameters, warnings);
273 property.setParameters(parameters);
274 return new Result<T>(property, warnings);
275 }
276
277 /**
278 * <p>
279 * Sanitizes a property's parameters before the property is written.
280 * </p>
281 * <p>
282 * This method should be overridden by child classes that wish to tweak the
283 * property's parameters before the property is written. The default
284 * implementation of this method does nothing.
285 * </p>
286 * @param property the property
287 * @param copy the list of parameters to make modifications to (it is a copy
288 * of the property's parameters)
289 * @param version the version of the vCard that is being generated
290 * @param vcard the vCard that the property belongs to
291 */
292 protected void _prepareParameters(T property, VCardParameters copy, VCardVersion version, VCard vcard) {
293 //do nothing
294 }
295
296 /**
297 * <p>
298 * Determines the property's default data type.
299 * </p>
300 * <p>
301 * When writing a plain-text vCard, if the data type of a property instance
302 * (as determined by the {@link #dataType} method) matches the default data
303 * type, then a VALUE parameter will *not* be written.
304 * </p>
305 * <p>
306 * When parsing a plain-text vCard, if a property has no VALUE parameter,
307 * then the property's default data type will be passed into the
308 * {@link #parseText} method.
309 * </p>
310 * @param version the vCard version
311 * @return the default data type or null if unknown
312 */
313 protected abstract VCardDataType _defaultDataType(VCardVersion version);
314
315 /**
316 * <p>
317 * Determines the data type of a property instance.
318 * </p>
319 * <p>
320 * This method should be overridden by child classes if a property's data
321 * type changes depending on its value. The default implementation of this
322 * method calls {@link #_defaultDataType}.
323 * </p>
324 * @param property the property
325 * @param version the version of the vCard that is being generated
326 * @return the data type or null if unknown
327 */
328 protected VCardDataType _dataType(T property, VCardVersion version) {
329 return _defaultDataType(version);
330 }
331
332 /**
333 * Marshals a property's value to a string.
334 * @param property the property
335 * @param version the version of the vCard that is being generated
336 * @return the marshalled value
337 * @throws SkipMeException if the property should not be written to the data
338 * stream
339 */
340 protected abstract String _writeText(T property, VCardVersion version);
341
342 /**
343 * <p>
344 * Marshals a property's value to an XML element (xCard).
345 * <p>
346 * <p>
347 * This method should be overridden by child classes that wish to support
348 * xCard. The default implementation of this method will append one child
349 * element to the property's XML element. The child element's name will be
350 * that of the property's data type (retrieved using the {@link #dataType}
351 * method), and the child element's text content will be set to the
352 * property's marshalled plain-text value (retrieved using the
353 * {@link #writeText} method).
354 * </p>
355 * @param property the property
356 * @param element the property's XML element
357 * @throws SkipMeException if the property should not be written to the data
358 * stream
359 */
360 protected void _writeXml(T property, XCardElement element) {
361 String value = writeText(property, VCardVersion.V4_0);
362 VCardDataType dataType = dataType(property, VCardVersion.V4_0);
363 element.append(dataType, value);
364 }
365
366 /**
367 * <p>
368 * Marshals a property's value to a JSON data stream (jCard).
369 * </p>
370 * <p>
371 * This method should be overridden by child classes that wish to support
372 * jCard. The default implementation of this method will create a jCard
373 * property that has a single JSON string value (generated by the
374 * {@link #writeText} method).
375 * </p>
376 * @param property the property
377 * @return the marshalled value
378 * @throws SkipMeException if the property should not be written to the data
379 * stream
380 */
381 protected JCardValue _writeJson(T property) {
382 String value = writeText(property, VCardVersion.V4_0);
383 return JCardValue.single(value);
384 }
385
386 /**
387 * Unmarshals a property from a plain-text vCard.
388 * @param value the value as read off the wire
389 * @param dataType the data type of the property value. The property's VALUE
390 * parameter is used to determine the data type. If the property has no
391 * VALUE parameter, then this parameter will be set to the property's
392 * default datatype, as determined by the {@link #defaultDataType} method.
393 * Note that the VALUE parameter is removed from the property's parameter
394 * list after it has been read.
395 * @param version the version of the vCard that is being parsed
396 * @param parameters the parsed parameters. These parameters will be
397 * assigned to the property object once this method returns. Therefore, do
398 * not assign any parameters to the property object itself whilst inside of
399 * this method, or else they will be overwritten.
400 * @param warnings allows the programmer to alert the user to any
401 * note-worthy (but non-critical) issues that occurred during the
402 * unmarshalling process
403 * @return the unmarshalled property object
404 * @throws CannotParseException if the marshaller could not parse the
405 * property's value
406 * @throws SkipMeException if the property should not be added to the final
407 * {@link VCard} object
408 */
409 protected abstract T _parseText(String value, VCardDataType dataType, VCardVersion version, VCardParameters parameters, List<String> warnings);
410
411 /**
412 * <p>
413 * Unmarshals a property from an XML document (xCard).
414 * </p>
415 * <p>
416 * This method should be overridden by child classes that wish to support
417 * xCard. The default implementation of this method will find the first
418 * child element with the xCard namespace. The element's name will be used
419 * as the property's data type and its text content will be passed into the
420 * {@link #_parseText} method. If no such child element is found, then the
421 * parent element's text content will be passed into {@link #_parseText} and
422 * the data type will be null.
423 * </p>
424 * @param element the property's XML element
425 * @param parameters the parsed parameters. These parameters will be
426 * assigned to the property object once this method returns. Therefore, do
427 * not assign any parameters to the property object itself whilst inside of
428 * this method, or else they will be overwritten.
429 * @param warnings allows the programmer to alert the user to any
430 * note-worthy (but non-critical) issues that occurred during the
431 * unmarshalling process
432 * @return the unmarshalled property object
433 * @throws CannotParseException if the marshaller could not parse the
434 * property's value
435 * @throws SkipMeException if the property should not be added to the final
436 * {@link VCard} object
437 */
438 protected T _parseXml(XCardElement element, VCardParameters parameters, List<String> warnings) {
439 String value = null;
440 VCardDataType dataType = null;
441 Element rawElement = element.element();
442
443 //get the text content of the first child element with the xCard namespace
444 List<Element> children = XmlUtils.toElementList(rawElement.getChildNodes());
445 for (Element child : children) {
446 if (!element.version().getXmlNamespace().equals(child.getNamespaceURI())) {
447 continue;
448 }
449
450 dataType = VCardDataType.get(child.getLocalName());
451 value = child.getTextContent();
452 break;
453 }
454
455 if (dataType == null) {
456 //get the text content of the property element
457 value = rawElement.getTextContent();
458 }
459
460 value = escape(value);
461 return _parseText(value, dataType, element.version(), parameters, warnings);
462 }
463
464 /**
465 * <p>
466 * Unmarshals the property from an hCard (HTML document).
467 * </p>
468 * <p>
469 * This method should be overridden by child classes that wish to support
470 * hCard. The default implementation of this method will retrieve the HTML
471 * element's hCard value (as described in {@link HCardElement#value}), and
472 * pass it into the {@link #_parseText} method.
473 * </p>
474 * @param element the property's HTML element
475 * @param warnings allows the programmer to alert the user to any
476 * note-worthy (but non-critical) issues that occurred during the
477 * unmarshalling process
478 * @throws CannotParseException if the property value could not be parsed
479 * @throws SkipMeException if this property should NOT be added to the
480 * {@link VCard} object
481 * @throws EmbeddedVCardException if the value of this property is an
482 * embedded vCard (i.e. the AGENT property)
483 */
484 protected T _parseHtml(HCardElement element, List<String> warnings) {
485 String value = escape(element.value());
486 VCardParameters parameters = new VCardParameters();
487 T property = _parseText(value, null, VCardVersion.V3_0, parameters, warnings);
488 property.setParameters(parameters);
489 return property;
490 }
491
492 /**
493 * <p>
494 * Unmarshals a property from a JSON data stream (jCard).
495 * </p>
496 * <p>
497 * This method should be overridden by child classes that wish to support
498 * jCard. The default implementation of this method will convert the jCard
499 * property value to a string and pass it into the {@link #_parseText}
500 * method.
501 * </p>
502 *
503 * <hr>
504 *
505 * <p>
506 * The following paragraphs describe the way in which this method's default
507 * implementation converts a jCard value to a string:
508 * </p>
509 * <p>
510 * If the jCard value consists of a single, non-array, non-object value,
511 * then the value is converted to a string. Special characters (backslashes,
512 * commas, and semicolons) are escaped in order to simulate what the value
513 * might look like in a plain-text vCard.<br>
514 * <code>["x-foo", {}, "text", "the;value"] --> "the\;value"</code><br>
515 * <code>["x-foo", {}, "text", 2] --> "2"</code>
516 * </p>
517 * <p>
518 * If the jCard value consists of multiple, non-array, non-object values,
519 * then all the values are appended together in a single string, separated
520 * by commas. Special characters (backslashes, commas, and semicolons) are
521 * escaped for each value in order to prevent commas from being treated as
522 * delimiters, and to simulate what the value might look like in a
523 * plain-text vCard.<br>
524 * <code>["x-foo", {}, "text", "one", "two,three"] -->
525 * "one,two\,three"</code>
526 * </p>
527 * <p>
528 * If the jCard value is a single array, then this array is treated as a
529 * "structured value", and converted its plain-text representation. Special
530 * characters (backslashes, commas, and semicolons) are escaped for each
531 * value in order to prevent commas and semicolons from being treated as
532 * delimiters.<br>
533 * <code>["x-foo", {}, "text", ["one", ["two", "three"], "four;five"]]
534 * --> "one;two,three;four\;five"</code>
535 * </p>
536 * <p>
537 * If the jCard value starts with a JSON object, then it is converted to an
538 * empty string (JSON objects are not supported by this method).<br>
539 * <code>["x-foo", , "text", {"one": 1}] --> ""}</code>
540 * </p>
541 * <p>
542 * For all other cases, behavior is undefined.
543 * </p>
544 * @param value the property's JSON value
545 * @param dataType the data type
546 * @param parameters the parsed parameters. These parameters will be
547 * assigned to the property object once this method returns. Therefore, do
548 * not assign any parameters to the property object itself whilst inside of
549 * this method, or else they will be overwritten.
550 * @param warnings allows the programmer to alert the user to any
551 * note-worthy (but non-critical) issues that occurred during the
552 * unmarshalling process
553 * @return the unmarshalled property object
554 * @throws CannotParseException if the marshaller could not parse the
555 * property's value
556 * @throws SkipMeException if the property should not be added to the final
557 * {@link VCard} object
558 */
559 protected T _parseJson(JCardValue value, VCardDataType dataType, VCardParameters parameters, List<String> warnings) {
560 return _parseText(jcardValueToString(value), dataType, VCardVersion.V4_0, parameters, warnings);
561 }
562
563 private static String jcardValueToString(JCardValue value) {
564 if (value.getValues().size() > 1) {
565 List<String> multi = value.asMulti();
566 if (!multi.isEmpty()) {
567 return list(multi);
568 }
569 }
570
571 if (!value.getValues().isEmpty() && value.getValues().get(0).getArray() != null) {
572 List<List<String>> structured = value.asStructured();
573 if (!structured.isEmpty()) {
574 return structured(structured.toArray());
575 }
576 }
577
578 return escape(value.asSingle());
579 }
580
581 /**
582 * Unescapes all special characters that are escaped with a backslash, as
583 * well as escaped newlines.
584 * @param text the text to unescape
585 * @return the unescaped text
586 */
587 public static String unescape(String text) {
588 if (text == null) {
589 return null;
590 }
591
592 StringBuilder sb = null; //only instantiate the StringBuilder if the string needs to be modified
593 boolean escaped = false;
594 for (int i = 0; i < text.length(); i++) {
595 char ch = text.charAt(i);
596
597 if (escaped) {
598 if (sb == null) {
599 sb = new StringBuilder(text.length());
600 sb.append(text.substring(0, i - 1));
601 }
602
603 escaped = false;
604
605 if (ch == 'n' || ch == 'N') {
606 //newlines appear as "\n" or "\N" (see RFC 5545 p.46)
607 sb.append(NEWLINE);
608 continue;
609 }
610
611 sb.append(ch);
612 continue;
613 }
614
615 if (ch == '\\') {
616 escaped = true;
617 continue;
618 }
619
620 if (sb != null) {
621 sb.append(ch);
622 }
623 }
624 return (sb == null) ? text : sb.toString();
625 }
626
627 /**
628 * <p>
629 * Escapes all special characters within a vCard value. These characters
630 * are:
631 * </p>
632 * <ul>
633 * <li>backslashes ({@code \})</li>
634 * <li>commas ({@code ,})</li>
635 * <li>semi-colons ({@code ;})</li>
636 * </ul>
637 * <p>
638 * Newlines are not escaped by this method. They are escaped when the vCard
639 * is serialized (in the {@link VCardRawWriter} class).
640 * </p>
641 * @param text the text to escape
642 * @return the escaped text
643 */
644 public static String escape(String text) {
645 if (text == null) {
646 return null;
647 }
648
649 String chars = "\\,;";
650 StringBuilder sb = null; //only instantiate the StringBuilder if the string needs to be modified
651 for (int i = 0; i < text.length(); i++) {
652 char ch = text.charAt(i);
653 if (chars.indexOf(ch) >= 0) {
654 if (sb == null) {
655 sb = new StringBuilder(text.length());
656 sb.append(text.substring(0, i));
657 }
658 sb.append('\\');
659 }
660
661 if (sb != null) {
662 sb.append(ch);
663 }
664 }
665 return (sb == null) ? text : sb.toString();
666 }
667
668 /**
669 * Splits a string by a delimiter.
670 * @param string the string to split (e.g. "one,two,three")
671 * @param delimiter the delimiter (e.g. ",")
672 * @return the factory object
673 */
674 protected static Splitter split(String string, String delimiter) {
675 return new Splitter(string, delimiter);
676 }
677
678 /**
679 * Factory class for splitting strings.
680 */
681 protected static class Splitter {
682 private String string;
683 private String delimiter;
684 private boolean removeEmpties = false;
685 private boolean unescape = false;
686 private int limit = -1;
687
688 /**
689 * Creates a new splitter object.
690 * @param string the string to split (e.g. "one,two,three")
691 * @param delimiter the delimiter (e.g. ",")
692 */
693 public Splitter(String string, String delimiter) {
694 this.string = string;
695 this.delimiter = delimiter;
696 }
697
698 /**
699 * Sets whether to remove empty elements.
700 * @param removeEmpties true to remove empty elements, false not to
701 * (default is false)
702 * @return this
703 */
704 public Splitter removeEmpties(boolean removeEmpties) {
705 this.removeEmpties = removeEmpties;
706 return this;
707 }
708
709 /**
710 * Sets whether to unescape each split string.
711 * @param unescape true to unescape, false not to (default is false)
712 * @return this
713 */
714 public Splitter unescape(boolean unescape) {
715 this.unescape = unescape;
716 return this;
717 }
718
719 /**
720 * Sets the max number of split strings it should parse.
721 * @param limit the max number of split strings
722 * @return this
723 */
724 public Splitter limit(int limit) {
725 this.limit = limit;
726 return this;
727 }
728
729 /**
730 * Performs the split operation.
731 * @return the split string
732 */
733 public List<String> split() {
734 //from: http://stackoverflow.com/q/820172">http://stackoverflow.com/q/820172
735 String split[] = string.split("\\s*(?<!\\\\)" + Pattern.quote(delimiter) + "\\s*", limit);
736
737 List<String> list = new ArrayList<String>(split.length);
738 for (String s : split) {
739 if (s.length() == 0 && removeEmpties) {
740 continue;
741 }
742
743 if (unescape) {
744 s = VCardPropertyScribe.unescape(s);
745 }
746
747 list.add(s);
748 }
749 return list;
750 }
751 }
752
753 /**
754 * Parses a comma-separated list of values.
755 * @param value the string to parse (e.g. "one,two,th\,ree")
756 * @return the parsed values
757 */
758 protected static List<String> list(String value) {
759 if (value.length() == 0) {
760 return new ArrayList<String>(0);
761 }
762 return split(value, ",").unescape(true).split();
763 }
764
765 /**
766 * Writes a comma-separated list of values.
767 * @param values the values to write
768 * @return the list
769 */
770 protected static String list(Object... values) {
771 return list(Arrays.asList(values));
772 }
773
774 /**
775 * Writes a comma-separated list of values.
776 * @param values the values to write
777 * @return the list
778 */
779 protected static <T> String list(Collection<T> values) {
780 return join(values, ",", new JoinCallback<T>() {
781 public void handle(StringBuilder sb, T value) {
782 if (value == null) {
783 return;
784 }
785 sb.append(escape(value.toString()));
786 }
787 });
788 }
789
790 /**
791 * Parses a list of values that are delimited by semicolons. Unlike
792 * structured value components, semi-structured components cannot be
793 * multi-valued.
794 * @param value the string to parse (e.g. "one;two;three")
795 * @return the parsed values
796 */
797 protected static SemiStructuredIterator semistructured(String value) {
798 return semistructured(value, -1);
799 }
800
801 /**
802 * Parses a list of values that are delimited by semicolons. Unlike
803 * structured value components, semi-structured components cannot be
804 * multi-valued.
805 * @param value the string to parse (e.g. "one;two;three")
806 * @param limit the max number of components to parse
807 * @return the parsed values
808 */
809 protected static SemiStructuredIterator semistructured(String value, int limit) {
810 List<String> split = split(value, ";").unescape(true).limit(limit).split();
811 return new SemiStructuredIterator(split.iterator());
812 }
813
814 /**
815 * Parses a structured value.
816 * @param value the string to parse (e.g. "one;two,three;four")
817 * @return the parsed values
818 */
819 protected static StructuredIterator structured(String value) {
820 List<String> split = split(value, ";").split();
821 List<List<String>> components = new ArrayList<List<String>>(split.size());
822 for (String s : split) {
823 components.add(list(s));
824 }
825 return new StructuredIterator(components.iterator());
826 }
827
828 /**
829 * Provides an iterator for a jCard structured value.
830 * @param value the jCard value
831 * @return the parsed values
832 */
833 protected static StructuredIterator structured(JCardValue value) {
834 return new StructuredIterator(value.asStructured().iterator());
835 }
836
837 /**
838 * <p>
839 * Writes a structured value.
840 * </p>
841 * <p>
842 * This method accepts a list of {@link Object} instances.
843 * {@link Collection} objects will be treated as multi-valued components.
844 * Null objects will be treated as empty components. All other objects will
845 * have their {@code toString()} method invoked to generate the string
846 * value.
847 * </p>
848 * @param values the values to write
849 * @return the structured value string
850 */
851 protected static String structured(Object... values) {
852 return join(Arrays.asList(values), ";", new JoinCallback<Object>() {
853 public void handle(StringBuilder sb, Object value) {
854 if (value == null) {
855 return;
856 }
857
858 if (value instanceof Collection) {
859 Collection<?> list = (Collection<?>) value;
860 sb.append(list(list));
861 return;
862 }
863
864 sb.append(escape(value.toString()));
865 }
866 });
867 }
868
869 /**
870 * Iterates over the fields in a structured value.
871 */
872 protected static class StructuredIterator {
873 private final Iterator<List<String>> it;
874
875 /**
876 * Constructs a new structured iterator.
877 * @param it the iterator to wrap
878 */
879 public StructuredIterator(Iterator<List<String>> it) {
880 this.it = it;
881 }
882
883 /**
884 * Gets the first value of the next component.
885 * @return the first value, null if the value is an empty string, or
886 * null if there are no more components
887 */
888 public String nextString() {
889 if (!hasNext()) {
890 return null;
891 }
892
893 List<String> list = it.next();
894 if (list.isEmpty()) {
895 return null;
896 }
897
898 String value = list.get(0);
899 return (value.length() == 0) ? null : value;
900 }
901
902 /**
903 * Gets the next component.
904 * @return the next component, an empty list if the component is empty,
905 * or an empty list of there are no more components
906 */
907 public List<String> nextComponent() {
908 if (!hasNext()) {
909 return new ArrayList<String>(0); //the lists should be mutable so they can be directly assigned to the property object's fields
910 }
911
912 List<String> list = it.next();
913 if (list.size() == 1 && list.get(0).length() == 0) {
914 return new ArrayList<String>(0);
915 }
916
917 return list;
918 }
919
920 public boolean hasNext() {
921 return it.hasNext();
922 }
923 }
924
925 /**
926 * Iterates over the fields in a semi-structured value (a structured value
927 * whose components cannot be multi-valued).
928 */
929 protected static class SemiStructuredIterator {
930 private final Iterator<String> it;
931
932 /**
933 * Constructs a new structured iterator.
934 * @param it the iterator to wrap
935 */
936 public SemiStructuredIterator(Iterator<String> it) {
937 this.it = it;
938 }
939
940 /**
941 * Gets the next value.
942 * @return the next value, null if the value is an empty string, or null
943 * if there are no more values
944 */
945 public String next() {
946 if (!hasNext()) {
947 return null;
948 }
949
950 String value = it.next();
951 return (value.length() == 0) ? null : value;
952 }
953
954 public boolean hasNext() {
955 return it.hasNext();
956 }
957 }
958
959 /**
960 * Parses a date string.
961 * @param value the date string
962 * @return the factory object
963 */
964 protected static Date date(String value) {
965 return VCardDateFormatter.parse(value);
966 }
967
968 /**
969 * Formats a {@link Date} object as a string.
970 * @param date the date
971 * @return the factory object
972 */
973 protected static DateWriter date(Date date) {
974 return new DateWriter(date);
975 }
976
977 /**
978 * Factory class for writing dates.
979 */
980 protected static class DateWriter {
981 private Date date;
982 private boolean hasTime = true;
983 private boolean extended = false;
984 private boolean utc = true;
985
986 /**
987 * Creates a new date writer object.
988 * @param date the date to format
989 */
990 public DateWriter(Date date) {
991 this.date = date;
992 }
993
994 /**
995 * Sets whether to output the date's time component.
996 * @param hasTime true include the time, false if it's strictly a date
997 * (defaults to "true")
998 * @return this
999 */
1000 public DateWriter time(boolean hasTime) {
1001 this.hasTime = hasTime;
1002 return this;
1003 }
1004
1005 /**
1006 * Sets whether to use extended format or basic.
1007 * @param extended true to use extended format, false to use basic
1008 * (defaults to "false")
1009 * @return this
1010 */
1011 public DateWriter extended(boolean extended) {
1012 this.extended = extended;
1013 return this;
1014 }
1015
1016 /**
1017 * Sets whether to format the date in UTC time, or to include a UTC
1018 * offset.
1019 * @param utc true to format in UTC time, false to include the local
1020 * timezone's UTC offset
1021 * @return this
1022 */
1023 public DateWriter utc(boolean utc) {
1024 this.utc = utc;
1025 return this;
1026 }
1027
1028 /**
1029 * Creates the date string.
1030 * @return the date string
1031 */
1032 public String write() {
1033 ISOFormat format;
1034 if (hasTime) {
1035 if (utc) {
1036 format = extended ? ISOFormat.UTC_TIME_EXTENDED : ISOFormat.UTC_TIME_BASIC;
1037 } else {
1038 format = extended ? ISOFormat.TIME_EXTENDED : ISOFormat.TIME_BASIC;
1039 }
1040 } else {
1041 format = extended ? ISOFormat.DATE_EXTENDED : ISOFormat.DATE_BASIC;
1042 }
1043
1044 return VCardDateFormatter.format(date, format);
1045 }
1046 }
1047
1048 /**
1049 * Creates a {@link CannotParseException}, indicating that the XML elements
1050 * that the parser expected to find are missing from the property's XML
1051 * element.
1052 * @param dataTypes the expected data types (null for "unknown")
1053 */
1054 protected static CannotParseException missingXmlElements(VCardDataType... dataTypes) {
1055 String[] elements = new String[dataTypes.length];
1056 for (int i = 0; i < dataTypes.length; i++) {
1057 VCardDataType dataType = dataTypes[i];
1058 elements[i] = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
1059 }
1060 return missingXmlElements(elements);
1061 }
1062
1063 /**
1064 * Creates a {@link CannotParseException}, indicating that the XML elements
1065 * that the parser expected to find are missing from the property's XML
1066 * element.
1067 * @param elements the names of the expected XML elements.
1068 */
1069 protected static CannotParseException missingXmlElements(String... elements) {
1070 return new CannotParseException(0, Arrays.toString(elements));
1071 }
1072
1073 /**
1074 * Utility method for switching between the "PREF" and "TYPE=PREF"
1075 * parameters, depending on the target vCard version. Meant to be called
1076 * from a scribe's {@link #_prepareParameters} method.
1077 * @param property the property that's being marshalled
1078 * @param copy the parameters that are being marshalled
1079 * @param version the vCard version
1080 * @param vcard the vCard that's being marshalled
1081 */
1082 protected static void handlePrefParam(VCardProperty property, VCardParameters copy, VCardVersion version, VCard vcard) {
1083 switch (version) {
1084 case V2_1:
1085 case V3_0:
1086 copy.setPref(null);
1087
1088 //find the property with the lowest PREF value in the vCard
1089 VCardProperty mostPreferred = null;
1090 for (VCardProperty p : vcard.getProperties(property.getClass())) {
1091 Integer pref = p.getParameters().getPref();
1092 if (pref == null) {
1093 continue;
1094 }
1095
1096 if (mostPreferred == null || pref < mostPreferred.getParameters().getPref()) {
1097 mostPreferred = p;
1098 }
1099 }
1100
1101 if (property == mostPreferred) {
1102 copy.addType("pref");
1103 }
1104
1105 break;
1106 case V4_0:
1107 for (String type : property.getParameters().getTypes()) {
1108 if ("pref".equalsIgnoreCase(type)) {
1109 copy.removeType(type);
1110 copy.setPref(1);
1111 break;
1112 }
1113 }
1114 break;
1115 }
1116 }
1117
1118 /**
1119 * Represents the result of an unmarshal operation.
1120 * @author Michael Angstadt
1121 * @param <T> the unmarshalled property class
1122 */
1123 public static class Result<T extends VCardProperty> {
1124 private final T property;
1125 private final List<String> warnings;
1126
1127 /**
1128 * Creates a new result.
1129 * @param property the property object
1130 * @param warnings the warnings
1131 */
1132 public Result(T property, List<String> warnings) {
1133 this.property = property;
1134 this.warnings = warnings;
1135 }
1136
1137 /**
1138 * Gets the warnings.
1139 * @return the warnings
1140 */
1141 public List<String> getWarnings() {
1142 return warnings;
1143 }
1144
1145 /**
1146 * Gets the property object.
1147 * @return the property object
1148 */
1149 public T getProperty() {
1150 return property;
1151 }
1152 }
1153 }