001    package biweekly.property.marshaller;
002    
003    import java.util.ArrayList;
004    import java.util.Date;
005    import java.util.List;
006    
007    import biweekly.ICalDataType;
008    import biweekly.io.json.JCalValue;
009    import biweekly.io.xml.XCalElement;
010    import biweekly.parameter.ICalParameters;
011    import biweekly.property.RecurrenceDates;
012    import biweekly.util.Duration;
013    import biweekly.util.Period;
014    
015    /*
016     Copyright (c) 2013, Michael Angstadt
017     All rights reserved.
018    
019     Redistribution and use in source and binary forms, with or without
020     modification, are permitted provided that the following conditions are met: 
021    
022     1. Redistributions of source code must retain the above copyright notice, this
023     list of conditions and the following disclaimer. 
024     2. Redistributions in binary form must reproduce the above copyright notice,
025     this list of conditions and the following disclaimer in the documentation
026     and/or other materials provided with the distribution. 
027    
028     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
029     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
030     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
031     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
032     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
033     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
034     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
035     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
036     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
037     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
038     */
039    
040    /**
041     * Marshals {@link RecurrenceDates} properties.
042     * @author Michael Angstadt
043     */
044    public class RecurrenceDatesMarshaller extends ICalPropertyMarshaller<RecurrenceDates> {
045            public RecurrenceDatesMarshaller() {
046                    super(RecurrenceDates.class, "RDATE", ICalDataType.DATE_TIME);
047            }
048    
049            @Override
050            protected ICalDataType _dataType(RecurrenceDates property) {
051                    if (property.getDates() != null) {
052                            return property.hasTime() ? ICalDataType.DATE_TIME : ICalDataType.DATE;
053                    }
054                    if (property.getPeriods() != null) {
055                            return ICalDataType.PERIOD;
056                    }
057                    return getDefaultDataType();
058            }
059    
060            @Override
061            protected String _writeText(final RecurrenceDates property) {
062                    List<Date> dates = property.getDates();
063                    if (dates != null) {
064                            return list(dates, new ListCallback<Date>() {
065                                    public String asString(Date date) {
066                                            return date(date).time(property.hasTime()).tzid(property.getTimezoneId()).write();
067                                    }
068                            });
069                    }
070    
071                    List<Period> periods = property.getPeriods();
072                    if (periods != null) {
073                            return list(periods, new ListCallback<Period>() {
074                                    public String asString(Period period) {
075                                            StringBuilder sb = new StringBuilder();
076    
077                                            if (period.getStartDate() != null) {
078                                                    String date = date(period.getStartDate()).tzid(property.getTimezoneId()).write();
079                                                    sb.append(date);
080                                            }
081    
082                                            sb.append('/');
083    
084                                            if (period.getEndDate() != null) {
085                                                    String date = date(period.getEndDate()).tzid(property.getTimezoneId()).write();
086                                                    sb.append(date);
087                                            } else if (period.getDuration() != null) {
088                                                    sb.append(period.getDuration());
089                                            }
090    
091                                            return sb.toString();
092                                    }
093                            });
094                    }
095    
096                    return "";
097            }
098    
099            @Override
100            protected RecurrenceDates _parseText(String value, ICalDataType dataType, ICalParameters parameters, List<String> warnings) {
101                    return parse(list(value), dataType, parameters, warnings);
102            }
103    
104            @Override
105            protected void _writeXml(RecurrenceDates property, XCalElement element) {
106                    List<Date> dates = property.getDates();
107                    if (dates != null) {
108                            ICalDataType dataType = property.hasTime() ? ICalDataType.DATE_TIME : ICalDataType.DATE;
109                            if (dates.isEmpty()) {
110                                    element.append(dataType, "");
111                            } else {
112                                    for (Date date : dates) {
113                                            String dateStr = date(date).time(property.hasTime()).tzid(property.getTimezoneId()).extended(true).write();
114                                            element.append(dataType, dateStr);
115                                    }
116                            }
117                            return;
118                    }
119    
120                    List<Period> periods = property.getPeriods();
121                    if (periods != null) {
122                            if (periods.isEmpty()) {
123                                    element.append(ICalDataType.PERIOD, "");
124                            } else {
125                                    for (Period period : periods) {
126                                            XCalElement periodElement = element.append(ICalDataType.PERIOD);
127    
128                                            Date start = period.getStartDate();
129                                            if (start != null) {
130                                                    periodElement.append("start", date(start).tzid(property.getTimezoneId()).extended(true).write());
131                                            }
132    
133                                            Date end = period.getEndDate();
134                                            if (end != null) {
135                                                    periodElement.append("end", date(end).tzid(property.getTimezoneId()).extended(true).write());
136                                            }
137    
138                                            Duration duration = period.getDuration();
139                                            if (duration != null) {
140                                                    periodElement.append("duration", duration.toString());
141                                            }
142                                    }
143                            }
144                            return;
145                    }
146    
147                    element.append(defaultDataType, "");
148            }
149    
150            @Override
151            protected RecurrenceDates _parseXml(XCalElement element, ICalParameters parameters, List<String> warnings) {
152                    //parse as periods
153                    List<XCalElement> periodElements = element.children(ICalDataType.PERIOD);
154                    if (!periodElements.isEmpty()) {
155                            List<Period> periods = new ArrayList<Period>(periodElements.size());
156                            for (XCalElement periodElement : periodElements) {
157                                    String startStr = periodElement.first("start");
158                                    if (startStr == null) {
159                                            warnings.add("No start date found in time period, skipping.");
160                                            continue;
161                                    }
162    
163                                    Date start = null;
164                                    try {
165                                            start = date(startStr).tzid(parameters.getTimezoneId(), warnings).parse();
166                                    } catch (IllegalArgumentException e) {
167                                            warnings.add("Could not parse start date, skipping time period: " + startStr);
168                                            continue;
169                                    }
170    
171                                    String endStr = periodElement.first("end");
172                                    if (endStr != null) {
173                                            try {
174                                                    Date end = date(endStr).tzid(parameters.getTimezoneId(), warnings).parse();
175                                                    periods.add(new Period(start, end));
176                                            } catch (IllegalArgumentException e) {
177                                                    warnings.add("Could not parse end date, skipping time period: " + endStr);
178                                            }
179                                            continue;
180                                    }
181    
182                                    String durationStr = periodElement.first("duration");
183                                    if (durationStr != null) {
184                                            try {
185                                                    Duration duration = Duration.parse(durationStr);
186                                                    periods.add(new Period(start, duration));
187                                            } catch (IllegalArgumentException e) {
188                                                    warnings.add("Could not parse duration, skipping time period: " + durationStr);
189                                            }
190                                            continue;
191                                    }
192    
193                                    warnings.add("Start date has no accompanying end date or duration, skipping.");
194                            }
195                            return new RecurrenceDates(periods);
196                    }
197    
198                    //parse as dates
199                    List<String> dateStrs = element.all(ICalDataType.DATE_TIME);
200                    boolean hasTime = !dateStrs.isEmpty();
201                    dateStrs.addAll(element.all(ICalDataType.DATE));
202                    if (!dateStrs.isEmpty()) {
203                            List<Date> dates = new ArrayList<Date>(dateStrs.size());
204                            for (String dateStr : dateStrs) {
205                                    try {
206                                            Date date = date(dateStr).tzid(parameters.getTimezoneId(), warnings).parse();
207                                            dates.add(date);
208                                    } catch (IllegalArgumentException e) {
209                                            warnings.add("Skipping unparsable date: " + dateStr);
210                                    }
211                            }
212                            return new RecurrenceDates(dates, hasTime);
213                    }
214    
215                    throw missingXmlElements(ICalDataType.PERIOD, ICalDataType.DATE_TIME, ICalDataType.DATE);
216            }
217    
218            @Override
219            protected JCalValue _writeJson(RecurrenceDates property) {
220                    List<String> values = new ArrayList<String>();
221    
222                    List<Date> dates = property.getDates();
223                    List<Period> periods = property.getPeriods();
224                    if (dates != null) {
225                            for (Date date : dates) {
226                                    String dateStr = date(date).time(property.hasTime()).tzid(property.getTimezoneId()).extended(true).write();
227                                    values.add(dateStr);
228                            }
229                    } else if (periods != null) {
230                            for (Period period : property.getPeriods()) {
231                                    StringBuilder sb = new StringBuilder();
232                                    if (period.getStartDate() != null) {
233                                            String value = date(period.getStartDate()).tzid(property.getTimezoneId()).extended(true).write();
234                                            sb.append(value);
235                                    }
236    
237                                    sb.append('/');
238    
239                                    if (period.getEndDate() != null) {
240                                            String value = date(period.getEndDate()).tzid(property.getTimezoneId()).extended(true).write();
241                                            sb.append(value);
242                                    } else if (period.getDuration() != null) {
243                                            sb.append(period.getDuration());
244                                    }
245    
246                                    values.add(sb.toString());
247                            }
248                    }
249    
250                    if (values.isEmpty()) {
251                            values.add("");
252                    }
253                    return JCalValue.multi(values);
254            }
255    
256            @Override
257            protected RecurrenceDates _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, List<String> warnings) {
258                    return parse(value.asMulti(), dataType, parameters, warnings);
259            }
260    
261            private RecurrenceDates parse(List<String> valueStrs, ICalDataType dataType, ICalParameters parameters, List<String> warnings) {
262                    if (dataType == ICalDataType.PERIOD) {
263                            //parse as periods
264                            List<Period> periods = new ArrayList<Period>(valueStrs.size());
265                            for (String timePeriodStr : valueStrs) {
266                                    String timePeriodStrSplit[] = timePeriodStr.split("/");
267    
268                                    if (timePeriodStrSplit.length < 2) {
269                                            warnings.add("No end date or duration found, skipping time period: " + timePeriodStr);
270                                            continue;
271                                    }
272    
273                                    String startStr = timePeriodStrSplit[0];
274                                    Date start;
275                                    try {
276                                            start = date(startStr).tzid(parameters.getTimezoneId(), warnings).parse();
277                                    } catch (IllegalArgumentException e) {
278                                            warnings.add("Could not parse start date, skipping time period: " + timePeriodStr);
279                                            continue;
280                                    }
281    
282                                    String endStr = timePeriodStrSplit[1];
283                                    try {
284                                            Date end = date(endStr).tzid(parameters.getTimezoneId(), warnings).parse();
285                                            periods.add(new Period(start, end));
286                                    } catch (IllegalArgumentException e) {
287                                            //must be a duration
288                                            try {
289                                                    Duration duration = Duration.parse(endStr);
290                                                    periods.add(new Period(start, duration));
291                                            } catch (IllegalArgumentException e2) {
292                                                    warnings.add("Could not parse end date or duration value, skipping time period: " + timePeriodStr);
293                                                    continue;
294                                            }
295                                    }
296                            }
297                            return new RecurrenceDates(periods);
298                    }
299    
300                    //parse as dates
301                    boolean hasTime = (dataType == ICalDataType.DATE_TIME);
302                    List<Date> dates = new ArrayList<Date>(valueStrs.size());
303                    for (String s : valueStrs) {
304                            try {
305                                    Date date = date(s).tzid(parameters.getTimezoneId(), warnings).parse();
306                                    dates.add(date);
307                            } catch (IllegalArgumentException e) {
308                                    warnings.add("Skipping unparsable date: " + s);
309                            }
310                    }
311                    return new RecurrenceDates(dates, hasTime);
312            }
313    }