001 package biweekly.property.marshaller;
002
003 import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS;
004
005 import java.util.ArrayList;
006 import java.util.Date;
007 import java.util.List;
008 import java.util.TimeZone;
009 import java.util.regex.Pattern;
010
011 import javax.xml.namespace.QName;
012
013 import org.w3c.dom.Element;
014
015 import biweekly.ICalendar;
016 import biweekly.io.CannotParseException;
017 import biweekly.io.SkipMeException;
018 import biweekly.io.text.ICalWriter;
019 import biweekly.io.xml.XCalElement;
020 import biweekly.parameter.ICalParameters;
021 import biweekly.parameter.Value;
022 import biweekly.property.ICalProperty;
023 import biweekly.util.ICalDateFormatter;
024 import biweekly.util.ISOFormat;
025
026 /*
027 Copyright (c) 2013, Michael Angstadt
028 All rights reserved.
029
030 Redistribution and use in source and binary forms, with or without
031 modification, are permitted provided that the following conditions are met:
032
033 1. Redistributions of source code must retain the above copyright notice, this
034 list of conditions and the following disclaimer.
035 2. Redistributions in binary form must reproduce the above copyright notice,
036 this list of conditions and the following disclaimer in the documentation
037 and/or other materials provided with the distribution.
038
039 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
040 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
041 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
042 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
043 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
044 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
045 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
046 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
047 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
048 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
049 */
050
051 /**
052 * Base class for iCalendar property marshallers.
053 * @author Michael Angstadt
054 */
055 public abstract class ICalPropertyMarshaller<T extends ICalProperty> {
056 private static final String NEWLINE = System.getProperty("line.separator");
057 protected final Class<T> clazz;
058 protected final String propertyName;
059 protected final QName qname;
060
061 /**
062 * Creates a new marshaller.
063 * @param clazz the property class
064 * @param propertyName the property name (e.g. "VERSION")
065 */
066 public ICalPropertyMarshaller(Class<T> clazz, String propertyName) {
067 this(clazz, propertyName, new QName(XCAL_NS, propertyName.toLowerCase()));
068 }
069
070 /**
071 * Creates a new marshaller.
072 * @param clazz the property class
073 * @param propertyName the property name (e.g. "VERSION")
074 * @param qname the XML element name and namespace (used for xCal documents)
075 */
076 public ICalPropertyMarshaller(Class<T> clazz, String propertyName, QName qname) {
077 this.clazz = clazz;
078 this.propertyName = propertyName;
079 this.qname = qname;
080 }
081
082 /**
083 * Gets the property class.
084 * @return the property class
085 */
086 public Class<T> getPropertyClass() {
087 return clazz;
088 }
089
090 /**
091 * Gets the property name.
092 * @return the property name (e.g. "VERSION")
093 */
094 public String getPropertyName() {
095 return propertyName;
096 }
097
098 /**
099 * Gets this property's local name and namespace for xCal documents.
100 * @return the XML local name and namespace
101 */
102 public QName getQName() {
103 return qname;
104 }
105
106 /**
107 * Sanitizes a property's parameters (called before the property is
108 * written). Note that a copy of the parameters is returned so that the
109 * property object does not get modified.
110 * @param property the property
111 * @return the sanitized parameters
112 */
113 public final ICalParameters prepareParameters(T property) {
114 //make a copy because the property should not get modified when it is marshalled
115 ICalParameters copy = new ICalParameters(property.getParameters());
116 _prepareParameters(property, copy);
117 return copy;
118 }
119
120 /**
121 * Marshals a property's value to a string.
122 * @param property the property
123 * @return the marshalled value
124 * @throws SkipMeException if the property should not be written to the data
125 * stream
126 */
127 public final String writeText(T property) {
128 return _writeText(property);
129 }
130
131 /**
132 * Marshals a property's value to an XML element (xCal).
133 * @param property the property
134 * @param element the property's XML element
135 * @throws SkipMeException if the property should not be written to the data
136 * stream
137 */
138 public final void writeXml(T property, Element element) {
139 XCalElement xcalElement = new XCalElement(element);
140 _writeXml(property, xcalElement);
141 }
142
143 /**
144 * Unmarshals a property's value.
145 * @param value the value
146 * @param parameters the property's parameters
147 * @return the unmarshalled property object
148 * @throws CannotParseException if the marshaller could not parse the
149 * property's value
150 * @throws SkipMeException if the property should not be added to the final
151 * {@link ICalendar} object
152 */
153 public final Result<T> parseText(String value, ICalParameters parameters) {
154 List<String> warnings = new ArrayList<String>(0);
155 T property = _parseText(value, parameters, warnings);
156 property.setParameters(parameters);
157 return new Result<T>(property, warnings);
158 }
159
160 /**
161 * Unmarshals a property's value from an XML document (xCal).
162 * @param element the property's XML element
163 * @param parameters the property's parameters
164 * @return the unmarshalled property object
165 * @throws CannotParseException if the marshaller could not parse the
166 * property's value
167 * @throws SkipMeException if the property should not be added to the final
168 * {@link ICalendar} object
169 */
170 public final Result<T> parseXml(Element element, ICalParameters parameters) {
171 List<String> warnings = new ArrayList<String>(0);
172 T property = _parseXml(new XCalElement(element), parameters, warnings);
173 property.setParameters(parameters);
174 return new Result<T>(property, warnings);
175 }
176
177 /**
178 * Sanitizes a property's parameters (called before the property is
179 * written). This should be overridden by child classes when required.
180 * @param property the property
181 * @param copy the list of parameters to make modifications to (it is a copy
182 * of the property's parameters)
183 */
184 protected void _prepareParameters(T property, ICalParameters copy) {
185 //do nothing
186 }
187
188 /**
189 * Marshals a property's value to a string.
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 protected abstract String _writeText(T property);
196
197 /**
198 * Marshals a property's value to an XML element (xCal).
199 * @param property the property
200 * @param element the XML element
201 * @throws SkipMeException if the property should not be written to the data
202 * stream
203 */
204 protected void _writeXml(T property, XCalElement element) {
205 String value = writeText(property);
206 Value dataType = property.getParameters().getValue();
207 if (dataType == null) {
208 element.appendUnknown(value);
209 } else {
210 element.append(dataType, value);
211 }
212 }
213
214 /**
215 * Unmarshals a property's value.
216 * @param value the value
217 * @param parameters the property's parameters
218 * @param warnings allows the programmer to alert the user to any
219 * note-worthy (but non-critical) issues that occurred during the
220 * unmarshalling process
221 * @return the unmarshalled property object
222 * @throws CannotParseException if the marshaller could not parse the
223 * property's value
224 * @throws SkipMeException if the property should not be added to the final
225 * {@link ICalendar} object
226 */
227 protected abstract T _parseText(String value, ICalParameters parameters, List<String> warnings);
228
229 /**
230 * Unmarshals a property's value from an XML document (xCal).
231 * @param element the property's XML element
232 * @param parameters the property's parameters
233 * @param warnings allows the programmer to alert the user to any
234 * note-worthy (but non-critical) issues that occurred during the
235 * unmarshalling process
236 * @return the unmarshalled property object
237 * @throws CannotParseException if the marshaller could not parse the
238 * property's value
239 * @throws SkipMeException if the property should not be added to the final
240 * {@link ICalendar} object
241 */
242 protected T _parseXml(XCalElement element, ICalParameters parameters, List<String> warnings) {
243 throw new UnsupportedOperationException();
244 }
245
246 /**
247 * Unescapes all special characters that are escaped with a backslash, as
248 * well as escaped newlines.
249 * @param text the text to unescape
250 * @return the unescaped text
251 */
252 protected static String unescape(String text) {
253 StringBuilder sb = new StringBuilder(text.length());
254 boolean escaped = false;
255 for (int i = 0; i < text.length(); i++) {
256 char ch = text.charAt(i);
257 if (escaped) {
258 if (ch == 'n' || ch == 'N') {
259 //newlines appear as "\n" or "\N" (see RFC 2426 p.7)
260 sb.append(NEWLINE);
261 } else {
262 sb.append(ch);
263 }
264 escaped = false;
265 } else if (ch == '\\') {
266 escaped = true;
267 } else {
268 sb.append(ch);
269 }
270 }
271 return sb.toString();
272 }
273
274 /**
275 * Escapes all special characters within a iCalendar value.
276 * <p>
277 * These characters are:
278 * </p>
279 * <ul>
280 * <li>backslashes (<code>\</code>)</li>
281 * <li>commas (<code>,</code>)</li>
282 * <li>semi-colons (<code>;</code>)</li>
283 * <li>(newlines are escaped by {@link ICalWriter})</li>
284 * </ul>
285 * @param text the text to escape
286 * @return the escaped text
287 */
288 protected static String escape(String text) {
289 String chars = "\\,;";
290 for (int i = 0; i < chars.length(); i++) {
291 String ch = chars.substring(i, i + 1);
292 text = text.replace(ch, "\\" + ch);
293 }
294 return text;
295 }
296
297 /**
298 * Splits a string by a delimiter.
299 * @param string the string to split (e.g. "one,two,three")
300 * @param delimiter the delimiter (e.g. ",")
301 * @return the factory object
302 */
303 protected static Splitter split(String string, String delimiter) {
304 return new Splitter(string, delimiter);
305 }
306
307 /**
308 * Factory class for splitting strings.
309 */
310 protected static class Splitter {
311 private String string;
312 private String delimiter;
313 private boolean removeEmpties = false;
314 private boolean unescape = false;
315
316 /**
317 * Creates a new splitter object.
318 * @param string the string to split (e.g. "one,two,three")
319 * @param delimiter the delimiter (e.g. ",")
320 */
321 public Splitter(String string, String delimiter) {
322 this.string = string;
323 this.delimiter = delimiter;
324 }
325
326 /**
327 * Sets whether to remove empty elements.
328 * @param removeEmpties true to remove empty elements, false not to
329 * (default is false)
330 * @return this
331 */
332 public Splitter removeEmpties(boolean removeEmpties) {
333 this.removeEmpties = removeEmpties;
334 return this;
335 }
336
337 /**
338 * Sets whether to unescape each split string.
339 * @param unescape true to unescape, false not to (default is false)
340 * @return this
341 */
342 public Splitter unescape(boolean unescape) {
343 this.unescape = unescape;
344 return this;
345 }
346
347 /**
348 * Performs the split operation.
349 * @return the split string
350 */
351 public String[] split() {
352 //from: http://stackoverflow.com/q/820172">http://stackoverflow.com/q/820172
353 String split[] = string.split("\\s*(?<!\\\\)" + Pattern.quote(delimiter) + "\\s*", -1);
354
355 List<String> list = new ArrayList<String>(split.length);
356 for (String s : split) {
357 if (s.length() == 0 && removeEmpties) {
358 continue;
359 }
360
361 if (unescape) {
362 s = ICalPropertyMarshaller.unescape(s);
363 }
364
365 list.add(s);
366 }
367
368 return list.toArray(new String[0]);
369 }
370 }
371
372 /**
373 * Parses a comma-separated list of values.
374 * @param str the string to parse (e.g. "one,two,th\,ree")
375 * @return the parsed values
376 */
377 protected static String[] parseList(String str) {
378 return split(str, ",").removeEmpties(true).unescape(true).split();
379 }
380
381 /**
382 * Parses a component value.
383 * @param str the string to parse (e.g. "one;two,three;four")
384 * @return the parsed values
385 */
386 protected static String[][] parseComponent(String str) {
387 String split[] = split(str, ";").split();
388 String ret[][] = new String[split.length][];
389 int i = 0;
390 for (String s : split) {
391 String split2[] = parseList(s);
392 ret[i++] = split2;
393 }
394 return ret;
395 }
396
397 /**
398 * Parses a date string.
399 * @param value the date string
400 * @return the factory object
401 */
402 protected static DateParser date(String value) {
403 return new DateParser(value);
404 }
405
406 /**
407 * Formats a {@link Date} object as a string.
408 * @param date the date
409 * @return the factory object
410 */
411 protected static DateWriter date(Date date) {
412 return new DateWriter(date);
413 }
414
415 /**
416 * Factory class for parsing dates.
417 */
418 protected static class DateParser {
419 private String value;
420 private TimeZone timezone;
421
422 /**
423 * Creates a new date writer object.
424 * @param value the date string to parse
425 */
426 public DateParser(String value) {
427 this.value = value;
428 }
429
430 /**
431 * Sets the ID of the timezone to parse the date as (TZID parameter
432 * value). If the ID does not contain a "/" character, it will be
433 * ignored.
434 * @param timezoneId the timezone ID
435 * @return this
436 */
437 public DateParser tzid(String timezoneId) {
438 return tzid(timezoneId, null);
439 }
440
441 /**
442 * Sets the ID of the timezone to parse the date as (TZID parameter
443 * value). If the ID does not contain a "/" character, it will be
444 * ignored. If the ID is invalid, the date will be formatted according
445 * to the JVM's default timezone and a warning message will be added to
446 * the provided warnings list.
447 * @param timezoneId the timezone ID
448 * @param warnings if the ID is not recognized, a warning message will
449 * be added to this list
450 * @return this
451 */
452 public DateParser tzid(String timezoneId, List<String> warnings) {
453 if (timezoneId == null) {
454 timezone = null;
455 return this;
456 }
457
458 if (timezoneId.contains("/")) {
459 timezone = ICalDateFormatter.parseTimeZoneId(timezoneId);
460 if (timezone == null) {
461 timezone = TimeZone.getDefault();
462 if (warnings != null) {
463 warnings.add("Timezone ID not recognized, parsing with default timezone instead: " + timezoneId);
464 }
465 }
466 } else {
467 //TODO support VTIMEZONE
468 }
469 return this;
470 }
471
472 /**
473 * Sets the timezone to parse the date as.
474 * @param timezone the timezone
475 * @return this
476 */
477 public DateParser tz(TimeZone timezone) {
478 this.timezone = timezone;
479 return this;
480 }
481
482 /**
483 * Parses the date string.
484 * @return the parsed date
485 * @throws IllegalArgumentException if the date string is invalid
486 */
487 public Date parse() {
488 return ICalDateFormatter.parse(value, timezone);
489 }
490 }
491
492 /**
493 * Factory class for writing dates.
494 */
495 protected static class DateWriter {
496 private Date date;
497 private boolean hasTime = true;
498 private TimeZone timezone;
499 private boolean extended = false;
500
501 /**
502 * Creates a new date writer object.
503 * @param date the date to format
504 */
505 public DateWriter(Date date) {
506 this.date = date;
507 }
508
509 /**
510 * Sets whether to output the date's time component.
511 * @param hasTime true include the time, false if it's strictly a date
512 * (defaults to "true")
513 * @return this
514 */
515 public DateWriter time(boolean hasTime) {
516 this.hasTime = hasTime;
517 return this;
518 }
519
520 /**
521 * Sets the ID of the timezone to format the date as (TZID parameter
522 * value). If the ID does not contain a "/" character, it will be
523 * ignored. If the ID is invalid, the date will be formatted according
524 * to the JVM's default timezone. If no timezone is specified, the date
525 * will be formatted as UTC.
526 * @param timezoneId the timezone ID
527 * @return this
528 */
529 public DateWriter tzid(String timezoneId) {
530 if (timezoneId == null) {
531 timezone = null;
532 return this;
533 }
534
535 if (timezoneId.contains("/")) {
536 timezone = ICalDateFormatter.parseTimeZoneId(timezoneId);
537 } else {
538 //TODO support VTIMEZONE
539 timezone = TimeZone.getDefault();
540 }
541 return this;
542 }
543
544 /**
545 * Sets the timezone to format the date as. If no timezone is specified,
546 * the date will be formatted as UTC.
547 * @param timezone the timezone
548 * @return this
549 */
550 public DateWriter tz(TimeZone timezone) {
551 this.timezone = timezone;
552 return this;
553 }
554
555 /**
556 * Sets whether to use extended format or basic.
557 * @param extended true to use extended format, false to use basic
558 * (defaults to "false")
559 * @return this
560 */
561 public DateWriter extended(boolean extended) {
562 this.extended = extended;
563 return this;
564 }
565
566 /**
567 * Creates the date string.
568 * @return the date string
569 */
570 public String write() {
571 ISOFormat format;
572 if (hasTime) {
573 if (timezone == null) {
574 format = extended ? ISOFormat.UTC_TIME_EXTENDED : ISOFormat.UTC_TIME_BASIC;
575 } else {
576 format = extended ? ISOFormat.TIME_EXTENDED_WITHOUT_TZ : ISOFormat.TIME_BASIC_WITHOUT_TZ;
577 }
578 } else {
579 format = extended ? ISOFormat.DATE_EXTENDED : ISOFormat.DATE_BASIC;
580 }
581
582 return ICalDateFormatter.format(date, format, timezone);
583 }
584 }
585
586 /**
587 * Represents the result of a marshal or unmarshal operation.
588 * @author Michael Angstadt
589 * @param <T> the marshalled/unmarshalled value (e.g. "String" if a property
590 * was marshalled)
591 */
592 public static class Result<T> {
593 private final T value;
594 private final List<String> warnings;
595
596 /**
597 * Creates a new result.
598 * @param value the value
599 * @param warnings the warnings
600 */
601 public Result(T value, List<String> warnings) {
602 this.value = value;
603 this.warnings = warnings;
604 }
605
606 /**
607 * Gets the warnings.
608 * @return the warnings
609 */
610 public List<String> getWarnings() {
611 return warnings;
612 }
613
614 /**
615 * Gets the value.
616 * @return the value
617 */
618 public T getValue() {
619 return value;
620 }
621 }
622 }