001package biweekly.io.scribe.property; 002 003import java.util.ArrayList; 004import java.util.Arrays; 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.ICalVersion; 014import biweekly.Warning; 015import biweekly.io.CannotParseException; 016import biweekly.io.ParseContext; 017import biweekly.io.WriteContext; 018import biweekly.io.json.JCalValue; 019import biweekly.io.xml.XCalElement; 020import biweekly.io.xml.XCalNamespaceContext; 021import biweekly.parameter.ICalParameters; 022import biweekly.property.RecurrenceProperty; 023import biweekly.util.ICalDate; 024import biweekly.util.ListMultimap; 025import biweekly.util.Recurrence; 026import biweekly.util.Recurrence.ByDay; 027import biweekly.util.Recurrence.DayOfWeek; 028import biweekly.util.Recurrence.Frequency; 029import biweekly.util.XmlUtils; 030 031/* 032 Copyright (c) 2013-2015, Michael Angstadt 033 All rights reserved. 034 035 Redistribution and use in source and binary forms, with or without 036 modification, are permitted provided that the following conditions are met: 037 038 1. Redistributions of source code must retain the above copyright notice, this 039 list of conditions and the following disclaimer. 040 2. Redistributions in binary form must reproduce the above copyright notice, 041 this list of conditions and the following disclaimer in the documentation 042 and/or other materials provided with the distribution. 043 044 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 045 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 046 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 047 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 048 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 049 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 050 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 051 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 052 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 053 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 054 */ 055 056/** 057 * Marshals properties whose values are {@link Recurrence}. 058 * @param <T> the property class 059 * @author Michael Angstadt 060 */ 061public abstract class RecurrencePropertyScribe<T extends RecurrenceProperty> extends ICalPropertyScribe<T> { 062 private static final String FREQ = "FREQ"; 063 private static final String UNTIL = "UNTIL"; 064 private static final String COUNT = "COUNT"; 065 private static final String INTERVAL = "INTERVAL"; 066 private static final String BYSECOND = "BYSECOND"; 067 private static final String BYMINUTE = "BYMINUTE"; 068 private static final String BYHOUR = "BYHOUR"; 069 private static final String BYDAY = "BYDAY"; 070 private static final String BYMONTHDAY = "BYMONTHDAY"; 071 private static final String BYYEARDAY = "BYYEARDAY"; 072 private static final String BYWEEKNO = "BYWEEKNO"; 073 private static final String BYMONTH = "BYMONTH"; 074 private static final String BYSETPOS = "BYSETPOS"; 075 private static final String WKST = "WKST"; 076 077 public RecurrencePropertyScribe(Class<T> clazz, String propertyName) { 078 super(clazz, propertyName); 079 } 080 081 @Override 082 protected ICalDataType _defaultDataType(ICalVersion version) { 083 return ICalDataType.RECUR; 084 } 085 086 @Override 087 protected ICalParameters _prepareParameters(T property, WriteContext context) { 088 if (isInObservance(context)) { 089 return property.getParameters(); 090 } 091 092 Recurrence recur = property.getValue(); 093 if (recur == null || recur.getUntil() == null) { 094 return property.getParameters(); 095 } 096 097 return handleTzidParameter(property, recur.getUntil().hasTime(), context); 098 } 099 100 @Override 101 protected String _writeText(T property, WriteContext context) { 102 //null value 103 Recurrence recur = property.getValue(); 104 if (recur == null) { 105 return ""; 106 } 107 108 //iCal 2.0 109 if (context.getVersion() != ICalVersion.V1_0) { 110 ListMultimap<String, Object> components = buildComponents(property, context, false); 111 return object(components.getMap()); 112 } 113 114 //vCal 1.0 115 Frequency frequency = recur.getFrequency(); 116 if (frequency == null) { 117 return ""; 118 } 119 120 StringBuilder sb = new StringBuilder(); 121 122 Integer interval = recur.getInterval(); 123 if (interval == null) { 124 interval = 1; 125 } 126 127 switch (frequency) { 128 case YEARLY: 129 if (!recur.getByMonth().isEmpty()) { 130 sb.append("YM").append(interval); 131 for (Integer month : recur.getByMonth()) { 132 sb.append(' ').append(month); 133 } 134 } else { 135 sb.append("YD").append(interval); 136 for (Integer day : recur.getByYearDay()) { 137 sb.append(' ').append(day); 138 } 139 } 140 break; 141 142 case MONTHLY: 143 if (!recur.getByMonthDay().isEmpty()) { 144 sb.append("MD").append(interval); 145 for (Integer day : recur.getByMonthDay()) { 146 sb.append(' ').append(writeVCalInt(day)); 147 } 148 } else { 149 sb.append("MP").append(interval); 150 for (ByDay byDay : recur.getByDay()) { 151 DayOfWeek day = byDay.getDay(); 152 Integer prefix = byDay.getNum(); 153 if (prefix == null) { 154 prefix = 1; 155 } 156 157 sb.append(' ').append(writeVCalInt(prefix)).append(' ').append(day.getAbbr()); 158 } 159 } 160 break; 161 162 case WEEKLY: 163 sb.append("W").append(interval); 164 for (ByDay byDay : recur.getByDay()) { 165 sb.append(' ').append(byDay.getDay().getAbbr()); 166 } 167 break; 168 169 case DAILY: 170 sb.append("D").append(interval); 171 break; 172 173 case HOURLY: 174 sb.append("M").append(interval * 60); 175 break; 176 177 case MINUTELY: 178 sb.append("M").append(interval); 179 break; 180 181 default: 182 return ""; 183 } 184 185 Integer count = recur.getCount(); 186 ICalDate until = recur.getUntil(); 187 sb.append(' '); 188 189 if (count != null) { 190 sb.append('#').append(count); 191 } else if (until != null) { 192 String dateStr = date(until, property, context).extended(false).write(); 193 sb.append(dateStr); 194 } else { 195 sb.append("#0"); 196 } 197 198 return sb.toString(); 199 } 200 201 @Override 202 protected T _parseText(String value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { 203 final Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 204 205 if (value.length() == 0) { 206 return newInstance(builder.build()); 207 } 208 209 if (context.getVersion() != ICalVersion.V1_0) { 210 ListMultimap<String, String> rules = object(value); 211 212 List<Warning> warnings = context.getWarnings(); 213 parseFreq(rules, builder, warnings); 214 parseUntil(rules, builder, warnings); 215 parseCount(rules, builder, warnings); 216 parseInterval(rules, builder, warnings); 217 parseBySecond(rules, builder, warnings); 218 parseByMinute(rules, builder, warnings); 219 parseByHour(rules, builder, warnings); 220 parseByDay(rules, builder, warnings); 221 parseByMonthDay(rules, builder, warnings); 222 parseByYearDay(rules, builder, warnings); 223 parseByWeekNo(rules, builder, warnings); 224 parseByMonth(rules, builder, warnings); 225 parseBySetPos(rules, builder, warnings); 226 parseWkst(rules, builder, warnings); 227 parseXRules(rules, builder, warnings); //must be called last 228 229 T property = newInstance(builder.build()); 230 231 ICalDate until = property.getValue().getUntil(); 232 if (until != null) { 233 context.addDate(until, property, parameters); 234 } 235 236 return property; 237 } 238 239 String splitValues[] = value.toUpperCase().split("\\s+"); 240 241 //parse the frequency and interval from the first token (e.g. "W2") 242 String frequencyStr; 243 Integer interval; 244 { 245 String firstToken = splitValues[0]; 246 Pattern p = Pattern.compile("^([A-Z]+)(\\d+)$"); 247 Matcher m = p.matcher(firstToken); 248 if (!m.find()) { 249 throw new CannotParseException("Invalid token: " + firstToken); 250 } 251 252 frequencyStr = m.group(1); 253 interval = Integer.valueOf(m.group(2)); 254 splitValues = Arrays.copyOfRange(splitValues, 1, splitValues.length); 255 } 256 builder.interval(interval); 257 258 Integer count = null; 259 ICalDate until = null; 260 if (splitValues.length == 0) { 261 count = 2; 262 } else { 263 String lastToken = splitValues[splitValues.length - 1]; 264 if (lastToken.startsWith("#")) { 265 String countStr = lastToken.substring(1, lastToken.length()); 266 count = Integer.valueOf(countStr); 267 if (count == 0) { 268 //infinite 269 count = null; 270 } 271 272 splitValues = Arrays.copyOfRange(splitValues, 0, splitValues.length - 1); 273 } else { 274 try { 275 //see if the value is an "until" date 276 until = date(lastToken).parse(); 277 splitValues = Arrays.copyOfRange(splitValues, 0, splitValues.length - 1); 278 } catch (IllegalArgumentException e) { 279 //last token is a regular value 280 count = 2; 281 } 282 } 283 } 284 builder.count(count); 285 builder.until(until); 286 287 //determine what frequency enum to use and how to treat each tokenized value 288 Frequency frequency = null; 289 Handler<String> handler = null; 290 if ("YD".equals(frequencyStr)) { 291 frequency = Frequency.YEARLY; 292 handler = new Handler<String>() { 293 public void handle(String value) { 294 if (value == null) { 295 return; 296 } 297 298 Integer dayOfYear = Integer.valueOf(value); 299 builder.byYearDay(dayOfYear); 300 } 301 }; 302 } else if ("YM".equals(frequencyStr)) { 303 frequency = Frequency.YEARLY; 304 handler = new Handler<String>() { 305 public void handle(String value) { 306 if (value == null) { 307 return; 308 } 309 310 Integer month = Integer.valueOf(value); 311 builder.byMonth(month); 312 } 313 }; 314 } else if ("MD".equals(frequencyStr)) { 315 frequency = Frequency.MONTHLY; 316 handler = new Handler<String>() { 317 public void handle(String value) { 318 if (value == null) { 319 return; 320 } 321 322 Integer date = "LD".equals(value) ? -1 : parseVCalInt(value); 323 builder.byMonthDay(date); 324 } 325 }; 326 } else if ("MP".equals(frequencyStr)) { 327 frequency = Frequency.MONTHLY; 328 handler = new Handler<String>() { 329 private final List<Integer> nums = new ArrayList<Integer>(); 330 private final List<DayOfWeek> days = new ArrayList<DayOfWeek>(); 331 private boolean readNum = false; 332 333 public void handle(String value) { 334 if (value == null) { 335 //end of list 336 for (Integer num : nums) { 337 for (DayOfWeek day : days) { 338 builder.byDay(num, day); 339 } 340 } 341 return; 342 } 343 344 if (value.matches("\\d{4}")) { 345 readNum = false; 346 347 Integer hour = Integer.valueOf(value.substring(0, 2)); 348 builder.byHour(hour); 349 350 Integer minute = Integer.valueOf(value.substring(2, 4)); 351 builder.byMinute(minute); 352 return; 353 } 354 355 try { 356 Integer curNum = parseVCalInt(value); 357 358 if (!readNum) { 359 //reset lists, new segment 360 for (Integer num : nums) { 361 for (DayOfWeek day : days) { 362 builder.byDay(num, day); 363 } 364 } 365 nums.clear(); 366 days.clear(); 367 368 readNum = true; 369 } 370 371 nums.add(curNum); 372 } catch (NumberFormatException e) { 373 readNum = false; 374 375 DayOfWeek day = parseDay(value); 376 days.add(day); 377 } 378 } 379 }; 380 } else if ("W".equals(frequencyStr)) { 381 frequency = Frequency.WEEKLY; 382 handler = new Handler<String>() { 383 public void handle(String value) { 384 if (value == null) { 385 return; 386 } 387 388 DayOfWeek day = parseDay(value); 389 builder.byDay(day); 390 } 391 }; 392 } else if ("D".equals(frequencyStr)) { 393 frequency = Frequency.DAILY; 394 handler = new Handler<String>() { 395 public void handle(String value) { 396 if (value == null) { 397 return; 398 } 399 400 Integer hour = Integer.valueOf(value.substring(0, 2)); 401 builder.byHour(hour); 402 403 Integer minute = Integer.valueOf(value.substring(2, 4)); 404 builder.byMinute(minute); 405 } 406 }; 407 } else if ("M".equals(frequencyStr)) { 408 frequency = Frequency.MINUTELY; 409 handler = new Handler<String>() { 410 public void handle(String value) { 411 //TODO can this ever have values? 412 } 413 }; 414 } else { 415 throw new CannotParseException("Unrecognized frequency: " + frequencyStr); 416 } 417 418 builder.frequency(frequency); 419 420 //parse the rest of the tokens 421 for (String splitValue : splitValues) { 422 //TODO not sure how to handle the "$" symbol, ignore it 423 if (splitValue.endsWith("$")) { 424 context.addWarning(36, splitValue); 425 splitValue = splitValue.substring(0, splitValue.length() - 1); 426 } 427 428 handler.handle(splitValue); 429 } 430 handler.handle(null); 431 432 T property = newInstance(builder.build()); 433 if (until != null) { 434 context.addDate(until, property, parameters); 435 } 436 437 return property; 438 } 439 440 private int parseVCalInt(String value) { 441 int negate = 1; 442 if (value.endsWith("+")) { 443 value = value.substring(0, value.length() - 1); 444 } else if (value.endsWith("-")) { 445 value = value.substring(0, value.length() - 1); 446 negate = -1; 447 } 448 449 return Integer.parseInt(value) * negate; 450 } 451 452 private String writeVCalInt(Integer value) { 453 if (value > 0) { 454 return value + "+"; 455 } 456 457 if (value < 0) { 458 return Math.abs(value) + "-"; 459 } 460 461 return value + ""; 462 } 463 464 private DayOfWeek parseDay(String value) { 465 DayOfWeek day = DayOfWeek.valueOfAbbr(value); 466 if (day == null) { 467 throw new CannotParseException("Invalid day: " + value); 468 } 469 470 return day; 471 } 472 473 @Override 474 protected void _writeXml(T property, XCalElement element, WriteContext context) { 475 XCalElement recurElement = element.append(dataType(property, null)); 476 477 Recurrence recur = property.getValue(); 478 if (recur == null) { 479 return; 480 } 481 482 ListMultimap<String, Object> components = buildComponents(property, context, true); 483 for (Map.Entry<String, List<Object>> component : components) { 484 String name = component.getKey().toLowerCase(); 485 for (Object value : component.getValue()) { 486 recurElement.append(name, value.toString()); 487 } 488 } 489 } 490 491 @Override 492 protected T _parseXml(XCalElement element, ICalParameters parameters, ParseContext context) { 493 ICalDataType dataType = defaultDataType(context.getVersion()); 494 XCalElement value = element.child(dataType); 495 if (value == null) { 496 throw missingXmlElements(dataType); 497 } 498 499 ListMultimap<String, String> rules = new ListMultimap<String, String>(); 500 for (Element child : XmlUtils.toElementList(value.getElement().getChildNodes())) { 501 if (!XCalNamespaceContext.XCAL_NS.equals(child.getNamespaceURI())) { 502 continue; 503 } 504 505 String name = child.getLocalName().toUpperCase(); 506 String text = child.getTextContent(); 507 rules.put(name, text); 508 } 509 510 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 511 512 List<Warning> warnings = context.getWarnings(); 513 parseFreq(rules, builder, warnings); 514 parseUntil(rules, builder, warnings); 515 parseCount(rules, builder, warnings); 516 parseInterval(rules, builder, warnings); 517 parseBySecond(rules, builder, warnings); 518 parseByMinute(rules, builder, warnings); 519 parseByHour(rules, builder, warnings); 520 parseByDay(rules, builder, warnings); 521 parseByMonthDay(rules, builder, warnings); 522 parseByYearDay(rules, builder, warnings); 523 parseByWeekNo(rules, builder, warnings); 524 parseByMonth(rules, builder, warnings); 525 parseBySetPos(rules, builder, warnings); 526 parseWkst(rules, builder, warnings); 527 parseXRules(rules, builder, warnings); //must be called last 528 529 T property = newInstance(builder.build()); 530 531 ICalDate until = property.getValue().getUntil(); 532 if (until != null) { 533 context.addDate(until, property, parameters); 534 } 535 536 return property; 537 } 538 539 @Override 540 protected JCalValue _writeJson(T property, WriteContext context) { 541 Recurrence recur = property.getValue(); 542 if (recur == null) { 543 return JCalValue.object(new ListMultimap<String, Object>(0)); 544 } 545 546 ListMultimap<String, Object> components = buildComponents(property, context, true); 547 548 //lower-case all the keys 549 ListMultimap<String, Object> object = new ListMultimap<String, Object>(components.keySet().size()); 550 for (Map.Entry<String, List<Object>> entry : components) { 551 String key = entry.getKey().toLowerCase(); 552 object.putAll(key, entry.getValue()); 553 } 554 555 return JCalValue.object(object); 556 } 557 558 @Override 559 protected T _parseJson(JCalValue value, ICalDataType dataType, ICalParameters parameters, ParseContext context) { 560 Recurrence.Builder builder = new Recurrence.Builder((Frequency) null); 561 562 //upper-case the keys 563 ListMultimap<String, String> object = value.asObject(); 564 ListMultimap<String, String> rules = new ListMultimap<String, String>(object.keySet().size()); 565 for (Map.Entry<String, List<String>> entry : object) { 566 String key = entry.getKey().toUpperCase(); 567 rules.putAll(key, entry.getValue()); 568 } 569 570 List<Warning> warnings = context.getWarnings(); 571 parseFreq(rules, builder, warnings); 572 parseUntil(rules, builder, warnings); 573 parseCount(rules, builder, warnings); 574 parseInterval(rules, builder, warnings); 575 parseBySecond(rules, builder, warnings); 576 parseByMinute(rules, builder, warnings); 577 parseByHour(rules, builder, warnings); 578 parseByDay(rules, builder, warnings); 579 parseByMonthDay(rules, builder, warnings); 580 parseByYearDay(rules, builder, warnings); 581 parseByWeekNo(rules, builder, warnings); 582 parseByMonth(rules, builder, warnings); 583 parseBySetPos(rules, builder, warnings); 584 parseWkst(rules, builder, warnings); 585 parseXRules(rules, builder, warnings); //must be called last 586 587 T property = newInstance(builder.build()); 588 589 ICalDate until = property.getValue().getUntil(); 590 if (until != null) { 591 context.addDate(until, property, parameters); 592 } 593 594 return property; 595 } 596 597 /** 598 * Creates a new instance of the recurrence property. 599 * @param recur the recurrence value 600 * @return the new instance 601 */ 602 protected abstract T newInstance(Recurrence recur); 603 604 private void parseFreq(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 605 parseFirst(rules, FREQ, new Handler<String>() { 606 public void handle(String value) { 607 value = value.toUpperCase(); 608 try { 609 Frequency frequency = Frequency.valueOf(value); 610 builder.frequency(frequency); 611 } catch (IllegalArgumentException e) { 612 warnings.add(Warning.parse(7, FREQ, value)); 613 } 614 } 615 }); 616 } 617 618 private void parseUntil(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 619 parseFirst(rules, UNTIL, new Handler<String>() { 620 public void handle(String value) { 621 try { 622 ICalDate date = date(value).parse(); 623 builder.until(date); 624 } catch (IllegalArgumentException e) { 625 warnings.add(Warning.parse(7, UNTIL, value)); 626 } 627 } 628 }); 629 } 630 631 private void parseCount(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 632 parseFirst(rules, COUNT, new Handler<String>() { 633 public void handle(String value) { 634 try { 635 builder.count(Integer.valueOf(value)); 636 } catch (NumberFormatException e) { 637 warnings.add(Warning.parse(7, COUNT, value)); 638 } 639 } 640 }); 641 } 642 643 private void parseInterval(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 644 parseFirst(rules, INTERVAL, new Handler<String>() { 645 public void handle(String value) { 646 try { 647 builder.interval(Integer.valueOf(value)); 648 } catch (NumberFormatException e) { 649 warnings.add(Warning.parse(7, INTERVAL, value)); 650 } 651 } 652 }); 653 } 654 655 private void parseBySecond(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 656 parseIntegerList(BYSECOND, rules, warnings, new Handler<Integer>() { 657 public void handle(Integer value) { 658 builder.bySecond(value); 659 } 660 }); 661 } 662 663 private void parseByMinute(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 664 parseIntegerList(BYMINUTE, rules, warnings, new Handler<Integer>() { 665 public void handle(Integer value) { 666 builder.byMinute(value); 667 } 668 }); 669 } 670 671 private void parseByHour(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 672 parseIntegerList(BYHOUR, rules, warnings, new Handler<Integer>() { 673 public void handle(Integer value) { 674 builder.byHour(value); 675 } 676 }); 677 } 678 679 private void parseByDay(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) { 680 Pattern p = Pattern.compile("^([-+]?\\d+)?(.*)$"); 681 for (String value : rules.removeAll(BYDAY)) { 682 Matcher m = p.matcher(value); 683 if (!m.find()) { 684 //this should never happen 685 //the regex contains a "match-all" pattern and should never not find anything 686 warnings.add(Warning.parse(7, BYDAY, value)); 687 continue; 688 } 689 690 String dayStr = m.group(2); 691 DayOfWeek day = DayOfWeek.valueOfAbbr(dayStr); 692 if (day == null) { 693 warnings.add(Warning.parse(7, BYDAY, value)); 694 continue; 695 } 696 697 String prefixStr = m.group(1); 698 Integer prefix = (prefixStr == null) ? null : Integer.valueOf(prefixStr); //no need to catch NumberFormatException because the regex guarantees that it will be a number 699 700 builder.byDay(prefix, day); 701 } 702 } 703 704 private void parseByMonthDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 705 parseIntegerList(BYMONTHDAY, rules, warnings, new Handler<Integer>() { 706 public void handle(Integer value) { 707 builder.byMonthDay(value); 708 } 709 }); 710 } 711 712 private void parseByYearDay(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 713 parseIntegerList(BYYEARDAY, rules, warnings, new Handler<Integer>() { 714 public void handle(Integer value) { 715 builder.byYearDay(value); 716 } 717 }); 718 } 719 720 private void parseByWeekNo(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 721 parseIntegerList(BYWEEKNO, rules, warnings, new Handler<Integer>() { 722 public void handle(Integer value) { 723 builder.byWeekNo(value); 724 } 725 }); 726 } 727 728 private void parseByMonth(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 729 parseIntegerList(BYMONTH, rules, warnings, new Handler<Integer>() { 730 public void handle(Integer value) { 731 builder.byMonth(value); 732 } 733 }); 734 } 735 736 private void parseBySetPos(ListMultimap<String, String> rules, final Recurrence.Builder builder, List<Warning> warnings) { 737 parseIntegerList(BYSETPOS, rules, warnings, new Handler<Integer>() { 738 public void handle(Integer value) { 739 builder.bySetPos(value); 740 } 741 }); 742 } 743 744 private void parseWkst(ListMultimap<String, String> rules, final Recurrence.Builder builder, final List<Warning> warnings) { 745 parseFirst(rules, WKST, new Handler<String>() { 746 public void handle(String value) { 747 DayOfWeek day = DayOfWeek.valueOfAbbr(value); 748 if (day == null) { 749 warnings.add(Warning.parse(7, WKST, value)); 750 return; 751 } 752 753 builder.workweekStarts(day); 754 } 755 }); 756 } 757 758 private void parseXRules(ListMultimap<String, String> rules, Recurrence.Builder builder, List<Warning> warnings) { 759 for (Map.Entry<String, List<String>> rule : rules) { 760 String name = rule.getKey(); 761 for (String value : rule.getValue()) { 762 builder.xrule(name, value); 763 } 764 } 765 } 766 767 private ListMultimap<String, Object> buildComponents(T property, WriteContext context, boolean extended) { 768 ListMultimap<String, Object> components = new ListMultimap<String, Object>(); 769 Recurrence recur = property.getValue(); 770 771 //FREQ must come first 772 if (recur.getFrequency() != null) { 773 components.put(FREQ, recur.getFrequency().name()); 774 } 775 776 ICalDate until = recur.getUntil(); 777 if (until != null) { 778 String dateStr; 779 if (isInObservance(context)) { 780 dateStr = date(until).observance(true).extended(extended).write(); 781 } else { 782 dateStr = date(until, property, context).extended(extended).write(); 783 } 784 components.put(UNTIL, dateStr); 785 } 786 787 if (recur.getCount() != null) { 788 components.put(COUNT, recur.getCount()); 789 } 790 791 if (recur.getInterval() != null) { 792 components.put(INTERVAL, recur.getInterval()); 793 } 794 795 addIntegerListComponent(components, BYSECOND, recur.getBySecond()); 796 addIntegerListComponent(components, BYMINUTE, recur.getByMinute()); 797 addIntegerListComponent(components, BYHOUR, recur.getByHour()); 798 799 for (ByDay byDay : recur.getByDay()) { 800 Integer prefix = byDay.getNum(); 801 DayOfWeek day = byDay.getDay(); 802 803 String value = day.getAbbr(); 804 if (prefix != null) { 805 value = prefix + value; 806 } 807 components.put(BYDAY, value); 808 } 809 810 addIntegerListComponent(components, BYMONTHDAY, recur.getByMonthDay()); 811 addIntegerListComponent(components, BYYEARDAY, recur.getByYearDay()); 812 addIntegerListComponent(components, BYWEEKNO, recur.getByWeekNo()); 813 addIntegerListComponent(components, BYMONTH, recur.getByMonth()); 814 addIntegerListComponent(components, BYSETPOS, recur.getBySetPos()); 815 816 if (recur.getWorkweekStarts() != null) { 817 components.put(WKST, recur.getWorkweekStarts().getAbbr()); 818 } 819 820 for (Map.Entry<String, List<String>> entry : recur.getXRules().entrySet()) { 821 String name = entry.getKey(); 822 for (String value : entry.getValue()) { 823 components.put(name, value); 824 } 825 } 826 827 return components; 828 } 829 830 private void addIntegerListComponent(ListMultimap<String, Object> components, String name, List<Integer> values) { 831 for (Integer value : values) { 832 components.put(name, value); 833 } 834 } 835 836 private void parseFirst(ListMultimap<String, String> rules, String name, Handler<String> handler) { 837 List<String> values = rules.removeAll(name); 838 if (values.isEmpty()) { 839 return; 840 } 841 842 String value = values.get(0); 843 handler.handle(value); 844 } 845 846 private void parseIntegerList(String name, ListMultimap<String, String> rules, List<Warning> warnings, Handler<Integer> handler) { 847 List<String> values = rules.removeAll(name); 848 for (String value : values) { 849 try { 850 handler.handle(Integer.valueOf(value)); 851 } catch (NumberFormatException e) { 852 warnings.add(Warning.parse(8, name, value)); 853 } 854 } 855 } 856 857 private interface Handler<T> { 858 void handle(T value); 859 } 860}