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