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