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}