001package biweekly.io.scribe.property; 002 003import java.util.Date; 004import java.util.Iterator; 005import java.util.List; 006import java.util.Map; 007import java.util.regex.Matcher; 008import java.util.regex.Pattern; 009 010import org.w3c.dom.Element; 011 012import biweekly.ICalDataType; 013import biweekly.Warning; 014import biweekly.io.json.JCalValue; 015import biweekly.io.xml.XCalElement; 016import biweekly.io.xml.XCalNamespaceContext; 017import biweekly.parameter.ICalParameters; 018import biweekly.property.RecurrenceProperty; 019import biweekly.util.ICalDateFormat; 020import biweekly.util.ListMultimap; 021import biweekly.util.Recurrence; 022import biweekly.util.Recurrence.DayOfWeek; 023import biweekly.util.Recurrence.Frequency; 024import biweekly.util.XmlUtils; 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 * Marshals properties whose values are {@link Recurrence}. 053 * @param <T> the property class 054 * @author Michael Angstadt 055 */ 056public abstract class RecurrencePropertyScribe<T extends RecurrenceProperty> extends ICalPropertyScribe<T> { 057 private static final String FREQ = "FREQ"; 058 private static final String UNTIL = "UNTIL"; 059 private static final String COUNT = "COUNT"; 060 private static final String INTERVAL = "INTERVAL"; 061 private static final String BYSECOND = "BYSECOND"; 062 private static final String BYMINUTE = "BYMINUTE"; 063 private static final String BYHOUR = "BYHOUR"; 064 private static final String BYDAY = "BYDAY"; 065 private static final String BYMONTHDAY = "BYMONTHDAY"; 066 private static final String BYYEARDAY = "BYYEARDAY"; 067 private static final String BYWEEKNO = "BYWEEKNO"; 068 private static final String BYMONTH = "BYMONTH"; 069 private static final String BYSETPOS = "BYSETPOS"; 070 private static final String WKST = "WKST"; 071 072 public RecurrencePropertyScribe(Class<T> clazz, String propertyName) { 073 super(clazz, propertyName, ICalDataType.RECUR); 074 } 075 076 @Override 077 protected String _writeText(T property) { 078 Recurrence recur = property.getValue(); 079 if (recur == null) { 080 return ""; 081 } 082 083 ListMultimap<String, Object> components = buildComponents(recur, false); 084 return object(components.getMap()); 085 } 086 087 @Override 088 protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, List<Warning> warnings) { 089 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 090 ListMultimap<String, String> rules = object(value); 091 092 parseFreq(rules, builder, warnings); 093 parseUntil(rules, builder, warnings); 094 parseCount(rules, builder, warnings); 095 parseInterval(rules, builder, warnings); 096 parseBySecond(rules, builder, warnings); 097 parseByMinute(rules, builder, warnings); 098 parseByHour(rules, builder, warnings); 099 parseByDay(rules, builder, warnings); 100 parseByMonthDay(rules, builder, warnings); 101 parseByYearDay(rules, builder, warnings); 102 parseByWeekNo(rules, builder, warnings); 103 parseByMonth(rules, builder, warnings); 104 parseBySetPos(rules, builder, warnings); 105 parseWkst(rules, builder, warnings); 106 parseXRules(rules, builder, warnings); //must be called last 107 108 return newInstance(builder.build()); 109 } 110 111 @Override 112 protected void _writeXml(T property, XCalElement element) { 113 XCalElement recurElement = element.append(dataType(property)); 114 115 Recurrence recur = property.getValue(); 116 if (recur == null) { 117 return; 118 } 119 120 ListMultimap<String, Object> components = buildComponents(recur, true); 121 for (Map.Entry<String, List<Object>> component : components) { 122 String name = component.getKey().toLowerCase(); 123 for (Object value : component.getValue()) { 124 recurElement.append(name, value.toString()); 125 } 126 } 127 } 128 129 @Override 130 protected T _parseXml(XCalElement element, ICalParameters parameters, List<Warning> warnings) { 131 XCalElement value = element.child(defaultDataType); 132 if (value == null) { 133 throw missingXmlElements(defaultDataType); 134 } 135 136 ListMultimap<String, String> rules = new ListMultimap<String, String>(); 137 for (Element child : XmlUtils.toElementList(value.getElement().getChildNodes())) { 138 if (!XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) { 139 continue; 140 } 141 142 String name = child.getLocalName().toUpperCase(); 143 String text = child.getTextContent(); 144 rules.put(name, text); 145 } 146 147 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 148 149 parseFreq(rules, builder, warnings); 150 parseUntil(rules, builder, warnings); 151 parseCount(rules, builder, warnings); 152 parseInterval(rules, builder, warnings); 153 parseBySecond(rules, builder, warnings); 154 parseByMinute(rules, builder, warnings); 155 parseByHour(rules, builder, warnings); 156 parseByDay(rules, builder, warnings); 157 parseByMonthDay(rules, builder, warnings); 158 parseByYearDay(rules, builder, warnings); 159 parseByWeekNo(rules, builder, warnings); 160 parseByMonth(rules, builder, warnings); 161 parseBySetPos(rules, builder, warnings); 162 parseWkst(rules, builder, warnings); 163 parseXRules(rules, builder, warnings); //must be called last 164 165 return newInstance(builder.build()); 166 } 167 168 @Override 169 protected JCalValue _writeJson(T property) { 170 Recurrence recur = property.getValue(); 171 if (recur == null) { 172 return JCalValue.object(new ListMultimap<String, Object>(0)); 173 } 174 175 ListMultimap<String, Object> components = buildComponents(recur, true); 176 177 //lower-case all the keys 178 ListMultimap<String, Object> object = new ListMultimap<String, Object>(components.keySet().size()); 179 for (Map.Entry<String, List<Object>> entry : components) { 180 String key = entry.getKey().toLowerCase(); 181 object.putAll(key, entry.getValue()); 182 } 183 184 return JCalValue.object(object); 185 } 186 187 @Override 188 protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, List<Warning> warnings) { 189 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 190 191 //upper-case the keys 192 ListMultimap<String, String> object = value.asObject(); 193 ListMultimap<String, String> rules = new ListMultimap<String, String>(object.keySet().size()); 194 for (Map.Entry<String, List<String>> entry : object) { 195 String key = entry.getKey().toUpperCase(); 196 rules.putAll(key, entry.getValue()); 197 } 198 199 parseFreq(rules, builder, warnings); 200 parseUntil(rules, builder, warnings); 201 parseCount(rules, builder, warnings); 202 parseInterval(rules, builder, warnings); 203 parseBySecond(rules, builder, warnings); 204 parseByMinute(rules, builder, warnings); 205 parseByHour(rules, builder, warnings); 206 parseByDay(rules, builder, warnings); 207 parseByMonthDay(rules, builder, warnings); 208 parseByYearDay(rules, builder, warnings); 209 parseByWeekNo(rules, builder, warnings); 210 parseByMonth(rules, builder, warnings); 211 parseBySetPos(rules, builder, warnings); 212 parseWkst(rules, builder, warnings); 213 parseXRules(rules, builder, warnings); //must be called last 214 215 return newInstance(builder.build()); 216 } 217 218 /** 219 * Creates a new instance of the recurrence property. 220 * @param recur the recurrence value 221 * @return the new instance 222 */ 223 protected abstract T newInstance(Recurrence recur); 224 225 private void parseFreq(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 226 parseFirst(rules, FREQ, new Handler<String>() { 227 public void handle(String value) { 228 value = value.toUpperCase(); 229 try { 230 Frequency frequency = Frequency.valueOf(value); 231 builder.frequency(frequency); 232 } catch (IllegalArgumentException e) { 233 warnings.add(Warning.parse(7, FREQ, value)); 234 } 235 } 236 }); 237 } 238 239 private void parseUntil(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 240 parseFirst(rules, UNTIL, new Handler<String>() { 241 public void handle(String value) { 242 try { 243 Date date = date(value).parse(); 244 boolean hasTime = ICalDateFormat.dateHasTime(value); 245 builder.until(date, hasTime); 246 } catch (IllegalArgumentException e) { 247 warnings.add(Warning.parse(7, UNTIL, value)); 248 } 249 } 250 }); 251 } 252 253 private void parseCount(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 254 parseFirst(rules, COUNT, new Handler<String>() { 255 public void handle(String value) { 256 try { 257 builder.count(Integer.valueOf(value)); 258 } catch (NumberFormatException e) { 259 warnings.add(Warning.parse(7, COUNT, value)); 260 } 261 } 262 }); 263 } 264 265 private void parseInterval(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 266 parseFirst(rules, INTERVAL, new Handler<String>() { 267 public void handle(String value) { 268 try { 269 builder.interval(Integer.valueOf(value)); 270 } catch (NumberFormatException e) { 271 warnings.add(Warning.parse(7, INTERVAL, value)); 272 } 273 } 274 }); 275 } 276 277 private void parseBySecond(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 278 parseIntegerList(BYSECOND, rules, warnings, new Handler<Integer>() { 279 public void handle(Integer value) { 280 builder.bySecond(value); 281 } 282 }); 283 } 284 285 private void parseByMinute(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 286 parseIntegerList(BYMINUTE, rules, warnings, new Handler<Integer>() { 287 public void handle(Integer value) { 288 builder.byMinute(value); 289 } 290 }); 291 } 292 293 private void parseByHour(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 294 parseIntegerList(BYHOUR, rules, warnings, new Handler<Integer>() { 295 public void handle(Integer value) { 296 builder.byHour(value); 297 } 298 }); 299 } 300 301 private void parseByDay(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) { 302 Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$"); 303 for (String value : rules.removeAll(BYDAY)) { 304 Matcher m = p.matcher(value); 305 if (!m.find()) { 306 //this should never happen 307 //the regex contains a "match-all" pattern and should never not find anything 308 warnings.add(Warning.parse(7, BYDAY, value)); 309 continue; 310 } 311 312 String dayStr = m.group(2); 313 DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr); 314 if (day == null) { 315 warnings.add(Warning.parse(7, BYDAY, value)); 316 continue; 317 } 318 319 String prefixStr = m.group(1); 320 Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr); //no need to catch NumberFormatException because the regex guarantees that it will be a number 321 322 builder.byDay(prefix, day); 323 } 324 } 325 326 private void parseByMonthDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 327 parseIntegerList(BYMONTHDAY, rules, warnings, new Handler<Integer>() { 328 public void handle(Integer value) { 329 builder.byMonthDay(value); 330 } 331 }); 332 } 333 334 private void parseByYearDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 335 parseIntegerList(BYYEARDAY, rules, warnings, new Handler<Integer>() { 336 public void handle(Integer value) { 337 builder.byYearDay(value); 338 } 339 }); 340 } 341 342 private void parseByWeekNo(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 343 parseIntegerList(BYWEEKNO, rules, warnings, new Handler<Integer>() { 344 public void handle(Integer value) { 345 builder.byWeekNo(value); 346 } 347 }); 348 } 349 350 private void parseByMonth(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 351 parseIntegerList(BYMONTH, rules, warnings, new Handler<Integer>() { 352 public void handle(Integer value) { 353 builder.byMonth(value); 354 } 355 }); 356 } 357 358 private void parseBySetPos(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 359 parseIntegerList(BYSETPOS, rules, warnings, new Handler<Integer>() { 360 public void handle(Integer value) { 361 builder.bySetPos(value); 362 } 363 }); 364 } 365 366 private void parseWkst(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 367 parseFirst(rules, WKST, new Handler<String>() { 368 public void handle(String value) { 369 DayOfWeek day = DayOfWeek.valueOfAbbr(value); 370 if (day == null) { 371 warnings.add(Warning.parse(7, WKST, value)); 372 return; 373 } 374 375 builder.workweekStarts(day); 376 } 377 }); 378 } 379 380 private void parseXRules(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) { 381 for (Map.Entry<String, List<String>> rule : rules) { 382 String name = rule.getKey(); 383 for (String value : rule.getValue()) { 384 builder.xrule(name, value); 385 } 386 } 387 } 388 389 private ListMultimap<String, Object> buildComponents(Recurrence recur, boolean extended) { 390 ListMultimap<String, Object> components = new ListMultimap<String, Object>(); 391 392 //FREQ must come first 393 if (recur.getFrequency() != null) { 394 components.put(FREQ, recur.getFrequency().name()); 395 } 396 397 if (recur.getUntil() != null) { 398 String s = date(recur.getUntil()).time(recur.hasTimeUntilDate()).extended(extended).write(); 399 components.put(UNTIL, s); 400 } 401 402 if (recur.getCount() != null) { 403 components.put(COUNT, recur.getCount()); 404 } 405 406 if (recur.getInterval() != null) { 407 components.put(INTERVAL, recur.getInterval()); 408 } 409 410 addIntegerListComponent(components, BYSECOND, recur.getBySecond()); 411 addIntegerListComponent(components, BYMINUTE, recur.getByMinute()); 412 addIntegerListComponent(components, BYHOUR, recur.getByHour()); 413 414 Iterator<Integer> prefixIt = recur.getByDayPrefixes().iterator(); 415 Iterator<DayOfWeek> dayIt = recur.getByDay().iterator(); 416 while (prefixIt.hasNext() && dayIt.hasNext()) { 417 Integer prefix = prefixIt.next(); 418 DayOfWeek day = dayIt.next(); 419 420 String value = day.getAbbr(); 421 if (prefix != null) { 422 value = prefix + value; 423 } 424 components.put(BYDAY, value); 425 } 426 427 addIntegerListComponent(components, BYMONTHDAY, recur.getByMonthDay()); 428 addIntegerListComponent(components, BYYEARDAY, recur.getByYearDay()); 429 addIntegerListComponent(components, BYWEEKNO, recur.getByWeekNo()); 430 addIntegerListComponent(components, BYMONTH, recur.getByMonth()); 431 addIntegerListComponent(components, BYSETPOS, recur.getBySetPos()); 432 433 if (recur.getWorkweekStarts() != null) { 434 components.put(WKST, recur.getWorkweekStarts().getAbbr()); 435 } 436 437 for (Map.Entry<String, List<String>> entry : recur.getXRules().entrySet()) { 438 String name = entry.getKey(); 439 for (String value : entry.getValue()) { 440 components.put(name, value); 441 } 442 } 443 444 return components; 445 } 446 447 private void addIntegerListComponent(ListMultimap<String, Object> components, String name, List<Integer> values) { 448 for (Integer value : values) { 449 components.put(name, value); 450 } 451 } 452 453 private void parseFirst(ListMultimap<String, String> rules, String name, Handler<String> handler) { 454 List<String> values = rules.removeAll(name); 455 if (values.isEmpty()) { 456 return; 457 } 458 459 String value = values.get(0); 460 handler.handle(value); 461 } 462 463 private void parseIntegerList(String name, ListMultimap<String, String> rules, List<Warning> warnings, Handler<Integer> handler) { 464 List<String> values = rules.removeAll(name); 465 for (String value : values) { 466 try { 467 handler.handle(Integer.valueOf(value)); 468 } catch (NumberFormatException e) { 469 warnings.add(Warning.parse(8, name, value)); 470 } 471 } 472 } 473 474 private interface Handler<T> { 475 void handle(T value); 476 } 477}