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