001package biweekly.util; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.Calendar; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.Date; 009import java.util.List; 010import java.util.Map; 011 012/* 013 Copyright (c) 2013-2015, Michael Angstadt 014 All rights reserved. 015 016 Redistribution and use in source and binary forms, with or without 017 modification, are permitted provided that the following conditions are met: 018 019 1. Redistributions of source code must retain the above copyright notice, this 020 list of conditions and the following disclaimer. 021 2. Redistributions in binary form must reproduce the above copyright notice, 022 this list of conditions and the following disclaimer in the documentation 023 and/or other materials provided with the distribution. 024 025 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 026 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 027 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 028 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 029 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 030 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 031 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 032 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 033 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 034 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 035 */ 036 037/** 038 * <p> 039 * Represents a recurrence rule value. 040 * </p> 041 * <p> 042 * This class is immutable. Use the inner class {@link Builder} to construct a 043 * new instance. 044 * </p> 045 * <p> 046 * <b>Code sample:</b> 047 * 048 * <pre class="brush:java"> 049 * Recurrence rrule = new Recurrence.Builder(Frequency.WEEKLY).interval(2).build(); 050 * Recurrence copy = new Recurrence.Builder(rrule).interval(3).build(); 051 * </pre> 052 * 053 * </p> 054 * @author Michael Angstadt 055 * @see <a href="http://tools.ietf.org/html/rfc5545#page-38">RFC 5545 056 * p.38-45</a> 057 */ 058public final class Recurrence { 059 private final Frequency frequency; 060 private final Integer interval; 061 private final Integer count; 062 private final ICalDate until; 063 private final List<Integer> bySecond; 064 private final List<Integer> byMinute; 065 private final List<Integer> byHour; 066 private final List<Integer> byMonthDay; 067 private final List<Integer> byYearDay; 068 private final List<Integer> byWeekNo; 069 private final List<Integer> byMonth; 070 private final List<Integer> bySetPos; 071 private final List<ByDay> byDay; 072 private final DayOfWeek workweekStarts; 073 private final Map<String, List<String>> xrules; 074 075 private Recurrence(Builder builder) { 076 frequency = builder.frequency; 077 interval = builder.interval; 078 count = builder.count; 079 until = builder.until; 080 bySecond = Collections.unmodifiableList(builder.bySecond); 081 byMinute = Collections.unmodifiableList(builder.byMinute); 082 byHour = Collections.unmodifiableList(builder.byHour); 083 byMonthDay = Collections.unmodifiableList(builder.byMonthDay); 084 byYearDay = Collections.unmodifiableList(builder.byYearDay); 085 byWeekNo = Collections.unmodifiableList(builder.byWeekNo); 086 byMonth = Collections.unmodifiableList(builder.byMonth); 087 bySetPos = Collections.unmodifiableList(builder.bySetPos); 088 byDay = Collections.unmodifiableList(builder.byDay); 089 workweekStarts = builder.workweekStarts; 090 091 Map<String, List<String>> map = builder.xrules.getMap(); 092 for (Map.Entry<String, List<String>> entry : map.entrySet()) { 093 String key = entry.getKey(); 094 List<String> value = entry.getValue(); 095 096 map.put(key, Collections.unmodifiableList(value)); 097 } 098 xrules = Collections.unmodifiableMap(map); 099 } 100 101 /** 102 * Gets the frequency. 103 * @return the frequency or null if not set 104 */ 105 public Frequency getFrequency() { 106 return frequency; 107 } 108 109 /** 110 * Gets the date that the recurrence stops. 111 * @return the date or null if not set 112 */ 113 public ICalDate getUntil() { 114 return (until == null) ? null : new ICalDate(until); 115 } 116 117 /** 118 * Gets the number of times the rule will be repeated. 119 * @return the number of times to repeat the rule or null if not set 120 */ 121 public Integer getCount() { 122 return count; 123 } 124 125 /** 126 * Gets how often the rule repeats, in relation to the frequency. 127 * @return the repetition interval or null if not set 128 */ 129 public Integer getInterval() { 130 return interval; 131 } 132 133 /** 134 * Gets the BYSECOND rule part. 135 * @return the BYSECOND rule part or empty list if not set 136 */ 137 public List<Integer> getBySecond() { 138 return bySecond; 139 } 140 141 /** 142 * Gets the BYMINUTE rule part. 143 * @return the BYMINUTE rule part or empty list if not set 144 */ 145 public List<Integer> getByMinute() { 146 return byMinute; 147 } 148 149 /** 150 * Gets the BYHOUR rule part. 151 * @return the BYHOUR rule part or empty list if not set 152 */ 153 public List<Integer> getByHour() { 154 return byHour; 155 } 156 157 /** 158 * Gets the day components of the BYDAY rule part. 159 * @return the day components of the BYDAY rule part or empty list if not 160 * set 161 */ 162 public List<ByDay> getByDay() { 163 return byDay; 164 } 165 166 /** 167 * Gets the BYMONTHDAY rule part. 168 * @return the BYMONTHDAY rule part or empty list if not set 169 */ 170 public List<Integer> getByMonthDay() { 171 return byMonthDay; 172 } 173 174 /** 175 * Gets the BYYEARDAY rule part. 176 * @return the BYYEARDAY rule part or empty list if not set 177 */ 178 public List<Integer> getByYearDay() { 179 return byYearDay; 180 } 181 182 /** 183 * Gets the BYWEEKNO rule part. 184 * @return the BYWEEKNO rule part or empty list if not set 185 */ 186 public List<Integer> getByWeekNo() { 187 return byWeekNo; 188 } 189 190 /** 191 * Gets the BYMONTH rule part. 192 * @return the BYMONTH rule part or empty list if not set 193 */ 194 public List<Integer> getByMonth() { 195 return byMonth; 196 } 197 198 /** 199 * Gets the BYSETPOS rule part. 200 * @return the BYSETPOS rule part or empty list if not set 201 */ 202 public List<Integer> getBySetPos() { 203 return bySetPos; 204 } 205 206 /** 207 * Gets the day that the work week starts. 208 * @return the day that the work week starts or null if not set 209 */ 210 public DayOfWeek getWorkweekStarts() { 211 return workweekStarts; 212 } 213 214 /** 215 * Gets the non-standard rule parts. 216 * @return the non-standard rule parts 217 */ 218 public Map<String, List<String>> getXRules() { 219 return xrules; 220 } 221 222 @Override 223 public int hashCode() { 224 final int prime = 31; 225 int result = 1; 226 result = prime * result + ((byDay == null) ? 0 : byDay.hashCode()); 227 result = prime * result + ((byHour == null) ? 0 : byHour.hashCode()); 228 result = prime * result + ((byMinute == null) ? 0 : byMinute.hashCode()); 229 result = prime * result + ((byMonth == null) ? 0 : byMonth.hashCode()); 230 result = prime * result + ((byMonthDay == null) ? 0 : byMonthDay.hashCode()); 231 result = prime * result + ((bySecond == null) ? 0 : bySecond.hashCode()); 232 result = prime * result + ((bySetPos == null) ? 0 : bySetPos.hashCode()); 233 result = prime * result + ((byWeekNo == null) ? 0 : byWeekNo.hashCode()); 234 result = prime * result + ((byYearDay == null) ? 0 : byYearDay.hashCode()); 235 result = prime * result + ((count == null) ? 0 : count.hashCode()); 236 result = prime * result + ((xrules == null) ? 0 : xrules.hashCode()); 237 result = prime * result + ((frequency == null) ? 0 : frequency.hashCode()); 238 result = prime * result + ((interval == null) ? 0 : interval.hashCode()); 239 result = prime * result + ((until == null) ? 0 : until.hashCode()); 240 result = prime * result + ((workweekStarts == null) ? 0 : workweekStarts.hashCode()); 241 return result; 242 } 243 244 @Override 245 public boolean equals(Object obj) { 246 if (this == obj) return true; 247 if (obj == null) return false; 248 if (getClass() != obj.getClass()) return false; 249 Recurrence other = (Recurrence) obj; 250 if (byDay == null) { 251 if (other.byDay != null) return false; 252 } else if (!byDay.equals(other.byDay)) return false; 253 if (byHour == null) { 254 if (other.byHour != null) return false; 255 } else if (!byHour.equals(other.byHour)) return false; 256 if (byMinute == null) { 257 if (other.byMinute != null) return false; 258 } else if (!byMinute.equals(other.byMinute)) return false; 259 if (byMonth == null) { 260 if (other.byMonth != null) return false; 261 } else if (!byMonth.equals(other.byMonth)) return false; 262 if (byMonthDay == null) { 263 if (other.byMonthDay != null) return false; 264 } else if (!byMonthDay.equals(other.byMonthDay)) return false; 265 if (bySecond == null) { 266 if (other.bySecond != null) return false; 267 } else if (!bySecond.equals(other.bySecond)) return false; 268 if (bySetPos == null) { 269 if (other.bySetPos != null) return false; 270 } else if (!bySetPos.equals(other.bySetPos)) return false; 271 if (byWeekNo == null) { 272 if (other.byWeekNo != null) return false; 273 } else if (!byWeekNo.equals(other.byWeekNo)) return false; 274 if (byYearDay == null) { 275 if (other.byYearDay != null) return false; 276 } else if (!byYearDay.equals(other.byYearDay)) return false; 277 if (count == null) { 278 if (other.count != null) return false; 279 } else if (!count.equals(other.count)) return false; 280 if (xrules == null) { 281 if (other.xrules != null) return false; 282 } else if (!xrules.equals(other.xrules)) return false; 283 if (frequency != other.frequency) return false; 284 if (interval == null) { 285 if (other.interval != null) return false; 286 } else if (!interval.equals(other.interval)) return false; 287 if (until == null) { 288 if (other.until != null) return false; 289 } else if (!until.equals(other.until)) return false; 290 if (workweekStarts != other.workweekStarts) return false; 291 return true; 292 } 293 294 /** 295 * Represents the frequency at which a recurrence rule repeats itself. 296 * @author Michael Angstadt 297 */ 298 public static enum Frequency { 299 SECONDLY, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY 300 } 301 302 /** 303 * Represents each of the seven days of the week. 304 * @author Michael Angstadt 305 */ 306 public static enum DayOfWeek { 307 MONDAY("MO", Calendar.MONDAY), TUESDAY("TU", Calendar.TUESDAY), WEDNESDAY("WE", Calendar.WEDNESDAY), THURSDAY("TH", Calendar.THURSDAY), FRIDAY("FR", Calendar.FRIDAY), SATURDAY("SA", Calendar.SATURDAY), SUNDAY("SU", Calendar.SUNDAY); 308 309 private final String abbr; 310 private final int calendarConstant; 311 312 private DayOfWeek(String abbr, int calendarConstant) { 313 this.abbr = abbr; 314 this.calendarConstant = calendarConstant; 315 } 316 317 /** 318 * Gets the day's abbreviation. 319 * @return the abbreviation (e.g. "MO" for Monday) 320 */ 321 public String getAbbr() { 322 return abbr; 323 } 324 325 public int getCalendarConstant() { 326 return calendarConstant; 327 } 328 329 /** 330 * Gets a day by its abbreviation. 331 * @param abbr the abbreviation (case-insensitive, e.g. "MO" for Monday) 332 * @return the day or null if not found 333 */ 334 public static DayOfWeek valueOfAbbr(String abbr) { 335 for (DayOfWeek day : values()) { 336 if (day.abbr.equalsIgnoreCase(abbr)) { 337 return day; 338 } 339 } 340 return null; 341 } 342 } 343 344 public static class ByDay { 345 private final Integer num; 346 private final DayOfWeek day; 347 348 public ByDay(DayOfWeek day) { 349 this(null, day); 350 } 351 352 public ByDay(Integer num, DayOfWeek day) { 353 this.num = num; 354 this.day = day; 355 } 356 357 public Integer getNum() { 358 return num; 359 } 360 361 public DayOfWeek getDay() { 362 return day; 363 } 364 365 @Override 366 public int hashCode() { 367 final int prime = 31; 368 int result = 1; 369 result = prime * result + ((day == null) ? 0 : day.hashCode()); 370 result = prime * result + ((num == null) ? 0 : num.hashCode()); 371 return result; 372 } 373 374 @Override 375 public boolean equals(Object obj) { 376 if (this == obj) return true; 377 if (obj == null) return false; 378 if (getClass() != obj.getClass()) return false; 379 ByDay other = (ByDay) obj; 380 if (day != other.day) return false; 381 if (num == null) { 382 if (other.num != null) return false; 383 } else if (!num.equals(other.num)) return false; 384 return true; 385 } 386 } 387 388 /** 389 * Constructs {@link Recurrence} objects. 390 * @author Michael Angstadt 391 */ 392 public static class Builder { 393 private Frequency frequency; 394 private Integer interval; 395 private Integer count; 396 private ICalDate until; 397 private List<Integer> bySecond; 398 private List<Integer> byMinute; 399 private List<Integer> byHour; 400 private List<ByDay> byDay; 401 private List<Integer> byMonthDay; 402 private List<Integer> byYearDay; 403 private List<Integer> byWeekNo; 404 private List<Integer> byMonth; 405 private List<Integer> bySetPos; 406 private DayOfWeek workweekStarts; 407 private ListMultimap<String, String> xrules; 408 409 /** 410 * Constructs a new builder. 411 * @param frequency the recurrence frequency 412 */ 413 public Builder(Frequency frequency) { 414 this.frequency = frequency; 415 bySecond = new ArrayList<Integer>(0); 416 byMinute = new ArrayList<Integer>(0); 417 byHour = new ArrayList<Integer>(0); 418 byDay = new ArrayList<ByDay>(0); 419 byMonthDay = new ArrayList<Integer>(0); 420 byYearDay = new ArrayList<Integer>(0); 421 byWeekNo = new ArrayList<Integer>(0); 422 byMonth = new ArrayList<Integer>(0); 423 bySetPos = new ArrayList<Integer>(0); 424 xrules = new ListMultimap<String, String>(0); 425 } 426 427 /** 428 * Constructs a new builder 429 * @param recur the recurrence object to copy from 430 */ 431 public Builder(Recurrence recur) { 432 frequency = recur.frequency; 433 interval = recur.interval; 434 count = recur.count; 435 until = recur.until; 436 bySecond = new ArrayList<Integer>(recur.bySecond); 437 byMinute = new ArrayList<Integer>(recur.byMinute); 438 byHour = new ArrayList<Integer>(recur.byHour); 439 byDay = new ArrayList<ByDay>(recur.byDay); 440 byMonthDay = new ArrayList<Integer>(recur.byMonthDay); 441 byYearDay = new ArrayList<Integer>(recur.byYearDay); 442 byWeekNo = new ArrayList<Integer>(recur.byWeekNo); 443 byMonth = new ArrayList<Integer>(recur.byMonth); 444 bySetPos = new ArrayList<Integer>(recur.bySetPos); 445 workweekStarts = recur.workweekStarts; 446 xrules = new ListMultimap<String, String>(recur.xrules); 447 } 448 449 /** 450 * Sets the frequency 451 * @param frequency the frequency 452 * @return this 453 */ 454 public Builder frequency(Frequency frequency) { 455 this.frequency = frequency; 456 return this; 457 } 458 459 /** 460 * Sets the date that the recurrence stops. Note that the UNTIL and 461 * COUNT fields cannot both be defined within the same rule. 462 * @param until the date 463 * @return this 464 */ 465 public Builder until(ICalDate until) { 466 this.until = (until == null) ? null : new ICalDate(until); 467 return this; 468 } 469 470 /** 471 * Sets the date that the recurrence stops. Note that the UNTIL and 472 * COUNT fields cannot both be defined within the same rule. 473 * @param until the date (time component will be included) 474 * @return this 475 */ 476 public Builder until(Date until) { 477 return until(until, true); 478 } 479 480 /** 481 * Sets the date that the recurrence stops. Note that the UNTIL and 482 * COUNT fields cannot both be defined within the same rule. 483 * @param until the date 484 * @param hasTime true to include the time component, false if it's 485 * strictly a date 486 * @return this 487 */ 488 public Builder until(Date until, boolean hasTime) { 489 return until(new ICalDate(until, hasTime)); 490 } 491 492 /** 493 * Gets the number of times the rule will be repeated. Note that the 494 * UNTIL and COUNT fields cannot both be defined within the same rule. 495 * @param count the number of times to repeat the rule 496 * @return this 497 */ 498 public Builder count(Integer count) { 499 this.count = count; 500 return this; 501 } 502 503 /** 504 * Gets how often the rule repeats, in relation to the frequency. 505 * @param interval the repetition interval 506 * @return this 507 */ 508 public Builder interval(Integer interval) { 509 this.interval = interval; 510 return this; 511 } 512 513 /** 514 * Adds one or more BYSECOND rule parts. 515 * @param seconds the seconds to add 516 * @return this 517 */ 518 public Builder bySecond(Integer... seconds) { 519 return bySecond(Arrays.asList(seconds)); 520 } 521 522 /** 523 * Adds one or more BYSECOND rule parts. 524 * @param seconds the seconds to add 525 * @return this 526 */ 527 public Builder bySecond(Collection<Integer> seconds) { 528 bySecond.addAll(seconds); 529 return this; 530 } 531 532 /** 533 * Adds one or more BYMINUTE rule parts. 534 * @param minutes the minutes to add 535 * @return this 536 */ 537 public Builder byMinute(Integer... minutes) { 538 return byMinute(Arrays.asList(minutes)); 539 } 540 541 /** 542 * Adds one or more BYMINUTE rule parts. 543 * @param minutes the minutes to add 544 * @return this 545 */ 546 public Builder byMinute(Collection<Integer> minutes) { 547 byMinute.addAll(minutes); 548 return this; 549 } 550 551 /** 552 * Adds one or more BYHOUR rule parts. 553 * @param hours the hours to add 554 * @return this 555 */ 556 public Builder byHour(Integer... hours) { 557 return byHour(Arrays.asList(hours)); 558 } 559 560 /** 561 * Adds one or more BYHOUR rule parts. 562 * @param hours the hours to add 563 * @return this 564 */ 565 public Builder byHour(Collection<Integer> hours) { 566 this.byHour.addAll(hours); 567 return this; 568 } 569 570 /** 571 * Adds one or more BYMONTHDAY rule parts. 572 * @param monthDays the month days to add 573 * @return this 574 */ 575 public Builder byMonthDay(Integer... monthDays) { 576 return byMonthDay(Arrays.asList(monthDays)); 577 } 578 579 /** 580 * Adds one or more BYMONTHDAY rule parts. 581 * @param monthDays the month days to add 582 * @return this 583 */ 584 public Builder byMonthDay(Collection<Integer> monthDays) { 585 byMonthDay.addAll(monthDays); 586 return this; 587 } 588 589 /** 590 * Adds one or more BYYEARDAY rule parts. 591 * @param yearDays the year days to add 592 * @return this 593 */ 594 public Builder byYearDay(Integer... yearDays) { 595 return byYearDay(Arrays.asList(yearDays)); 596 } 597 598 /** 599 * Adds one or more BYYEARDAY rule parts. 600 * @param yearDays the year days to add 601 * @return this 602 */ 603 public Builder byYearDay(Collection<Integer> yearDays) { 604 byYearDay.addAll(yearDays); 605 return this; 606 } 607 608 /** 609 * Adds one or more BYWEEKNO rule parts. 610 * @param weekNumbers the week numbers to add 611 * @return this 612 */ 613 public Builder byWeekNo(Integer... weekNumbers) { 614 return byWeekNo(Arrays.asList(weekNumbers)); 615 } 616 617 /** 618 * Adds one or more BYWEEKNO rule parts. 619 * @param weekNumbers the week numbers to add 620 * @return this 621 */ 622 public Builder byWeekNo(Collection<Integer> weekNumbers) { 623 byWeekNo.addAll(weekNumbers); 624 return this; 625 } 626 627 /** 628 * Adds one or more BYMONTH rule parts. 629 * @param months the months to add 630 * @return this 631 */ 632 public Builder byMonth(Integer... months) { 633 return byMonth(Arrays.asList(months)); 634 } 635 636 /** 637 * Adds one or more BYMONTH rule parts. 638 * @param months the months to add 639 * @return this 640 */ 641 public Builder byMonth(Collection<Integer> months) { 642 byMonth.addAll(months); 643 return this; 644 } 645 646 /** 647 * Adds one or more BYSETPOS rule parts. 648 * @param positions the values to add 649 * @return this 650 */ 651 public Builder bySetPos(Integer... positions) { 652 return bySetPos(Arrays.asList(positions)); 653 } 654 655 /** 656 * Adds one or more BYSETPOS rule parts. 657 * @param positions the values to add 658 * @return this 659 */ 660 public Builder bySetPos(Collection<Integer> positions) { 661 bySetPos.addAll(positions); 662 return this; 663 } 664 665 /** 666 * Adds one or more BYDAY rule parts. 667 * @param days the days to add 668 * @return this 669 */ 670 public Builder byDay(DayOfWeek... days) { 671 return byDay(Arrays.asList(days)); 672 } 673 674 /** 675 * Adds one or more BYDAY rule parts. 676 * @param days the days to add 677 * @return this 678 */ 679 public Builder byDay(Collection<DayOfWeek> days) { 680 for (DayOfWeek day : days) { 681 byDay(null, day); 682 } 683 return this; 684 } 685 686 /** 687 * Adds a BYDAY rule part. 688 * @param num the numeric component 689 * @param day the day to add 690 * @return this 691 */ 692 public Builder byDay(Integer num, DayOfWeek day) { 693 byDay.add(new ByDay(num, day)); 694 return this; 695 } 696 697 /** 698 * Sets the day that the work week starts. 699 * @param day the day 700 * @return this 701 */ 702 public Builder workweekStarts(DayOfWeek day) { 703 workweekStarts = day; 704 return this; 705 } 706 707 /** 708 * Adds a non-standard rule part. 709 * @param name the name 710 * @param value the value or null to remove the rule part 711 * @return this 712 */ 713 public Builder xrule(String name, String value) { 714 name = name.toUpperCase(); 715 716 if (value == null) { 717 xrules.removeAll(name); 718 } else { 719 xrules.put(name, value); 720 } 721 722 return this; 723 } 724 725 /** 726 * Builds the final {@link Recurrence} object. 727 * @return the object 728 */ 729 public Recurrence build() { 730 return new Recurrence(this); 731 } 732 } 733}