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    }