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     * //&quot;bi-weekly&quot;
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    }