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