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 }