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}