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