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