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}