001package biweekly.io;
002
003import static biweekly.property.ValuedProperty.getValue;
004import static biweekly.util.Google2445Utils.convert;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Calendar;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.Comparator;
012import java.util.Date;
013import java.util.Iterator;
014import java.util.List;
015import java.util.ListIterator;
016import java.util.Locale;
017import java.util.NoSuchElementException;
018import java.util.TimeZone;
019
020import biweekly.component.DaylightSavingsTime;
021import biweekly.component.Observance;
022import biweekly.component.StandardTime;
023import biweekly.component.VTimezone;
024import biweekly.property.DateStart;
025import biweekly.property.ExceptionDates;
026import biweekly.property.ExceptionRule;
027import biweekly.property.RecurrenceDates;
028import biweekly.property.RecurrenceRule;
029import biweekly.property.TimezoneId;
030import biweekly.property.TimezoneName;
031import biweekly.property.UtcOffsetProperty;
032import biweekly.util.ICalDate;
033import biweekly.util.Recurrence;
034
035import com.google.ical.iter.RecurrenceIterator;
036import com.google.ical.iter.RecurrenceIteratorFactory;
037import com.google.ical.values.DateTimeValue;
038import com.google.ical.values.DateTimeValueImpl;
039import com.google.ical.values.DateValue;
040import com.google.ical.values.RRule;
041
042/*
043 Copyright (c) 2013-2015, Michael Angstadt
044 All rights reserved.
045
046 Redistribution and use in source and binary forms, with or without
047 modification, are permitted provided that the following conditions are met: 
048
049 1. Redistributions of source code must retain the above copyright notice, this
050 list of conditions and the following disclaimer. 
051 2. Redistributions in binary form must reproduce the above copyright notice,
052 this list of conditions and the following disclaimer in the documentation
053 and/or other materials provided with the distribution. 
054
055 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
056 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
057 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
058 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
059 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
060 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
061 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
062 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
063 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
064 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
065 */
066
067/**
068 * A timezone that is based on an iCalendar {@link VTimezone} component.
069 * @author Michael Angstadt
070 */
071@SuppressWarnings("serial")
072public class ICalTimeZone extends TimeZone {
073        private final VTimezone component;
074
075        /**
076         * Creates a new timezone based on an iCalendar VTIMEZONE component.
077         * @param component the VTIMEZONE component to wrap
078         */
079        public ICalTimeZone(VTimezone component) {
080                this.component = component;
081
082                TimezoneId id = component.getTimezoneId();
083                if (id != null) {
084                        setID(id.getValue());
085                }
086        }
087
088        @Override
089        public String getDisplayName(boolean daylight, int style, Locale locale) {
090                List<Observance> observances = getSortedObservances();
091                ListIterator<Observance> it = observances.listIterator(observances.size());
092                while (it.hasPrevious()) {
093                        Observance observance = it.previous();
094
095                        if (daylight && observance instanceof DaylightSavingsTime) {
096                                List<TimezoneName> names = observance.getTimezoneNames();
097                                if (!names.isEmpty()) {
098                                        TimezoneName name = names.get(0);
099                                        return name.getValue();
100                                }
101                        }
102
103                        if (!daylight && observance instanceof StandardTime) {
104                                List<TimezoneName> names = observance.getTimezoneNames();
105                                if (!names.isEmpty()) {
106                                        TimezoneName name = names.get(0);
107                                        return name.getValue();
108                                }
109                        }
110                }
111
112                return super.getDisplayName(daylight, style, locale);
113        }
114
115        @Override
116        public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
117                int hour = millis / 1000 / 60 / 60;
118                millis -= hour * 1000 * 60 * 60;
119                int minute = millis / 1000 / 60;
120                millis -= minute * 1000 * 60;
121                int second = millis / 1000;
122
123                Observance observance = getObservance(year, month + 1, day, hour, minute, second);
124                if (observance == null) {
125                        //find the first observance that has a DTSTART property and a TZOFFSETFROM property
126                        for (Observance o : getSortedObservances()) {
127                                if (hasDateStart(o) && hasTimezoneOffsetFrom(o)) {
128                                        return o.getTimezoneOffsetFrom().getValue().toMillis();
129                                }
130                        }
131                        return 0;
132                }
133
134                return hasTimezoneOffsetTo(observance) ? observance.getTimezoneOffsetTo().getValue().toMillis() : 0;
135        }
136
137        @Override
138        public int getRawOffset() {
139                Observance observance = getObservance(new Date());
140                if (observance == null) {
141                        //return the offset of the first STANDARD component
142                        for (Observance o : getSortedObservances()) {
143                                if (o instanceof StandardTime && hasTimezoneOffsetTo(o)) {
144                                        return o.getTimezoneOffsetTo().getValue().toMillis();
145                                }
146                        }
147                        return 0;
148                }
149
150                UtcOffsetProperty offset;
151                if (observance instanceof StandardTime) {
152                        offset = observance.getTimezoneOffsetTo();
153                } else {
154                        offset = observance.getTimezoneOffsetFrom();
155                }
156
157                return offset.getValue().toMillis();
158        }
159
160        @Override
161        public boolean inDaylightTime(Date date) {
162                if (!useDaylightTime()) {
163                        return false;
164                }
165
166                Observance observance = getObservance(date);
167                return (observance == null) ? false : (observance instanceof DaylightSavingsTime);
168        }
169
170        /**
171         * @throws UnsupportedOperationException not supported by this
172         * implementation
173         */
174        @Override
175        public void setRawOffset(int offset) {
176                throw new UnsupportedOperationException("Unable to set the raw offset.  Modify the VTIMEZONE component instead.");
177        }
178
179        @Override
180        public boolean useDaylightTime() {
181                return !component.getDaylightSavingsTime().isEmpty();
182        }
183
184        /**
185         * Gets the timezone information of a date.
186         * @param date the date
187         * @return the timezone information
188         */
189        public Boundary getObservanceBoundary(Date date) {
190                Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
191                cal.setTime(date);
192                int year = cal.get(Calendar.YEAR);
193                int month = cal.get(Calendar.MONTH) + 1;
194                int day = cal.get(Calendar.DATE);
195                int hour = cal.get(Calendar.HOUR);
196                int minute = cal.get(Calendar.MINUTE);
197                int second = cal.get(Calendar.SECOND);
198
199                return getObservanceBoundary(year, month, day, hour, minute, second);
200        }
201
202        /**
203         * Gets the observance that a date is effected by.
204         * @param date the date
205         * @return the observance or null if an observance cannot be found
206         */
207        public Observance getObservance(Date date) {
208                Boundary boundary = getObservanceBoundary(date);
209                return (boundary == null) ? null : boundary.getObservanceIn();
210        }
211
212        /**
213         * Gets the VTIMEZONE component that is being wrapped. Modifications made to
214         * the component will effect this timezone object.
215         * @return the VTIMEZONE component
216         */
217        public VTimezone getComponent() {
218                return component;
219        }
220
221        /**
222         * Gets the observance that a date is effected by.
223         * @param year the year
224         * @param month the month (1-12)
225         * @param day the day of the month
226         * @param hour the hour
227         * @param minute the minute
228         * @param second the second
229         * @return the observance or null if an observance cannot be found
230         */
231        private Observance getObservance(int year, int month, int day, int hour, int minute, int second) {
232                Boundary boundary = getObservanceBoundary(year, month, day, hour, minute, second);
233                return (boundary == null) ? null : boundary.getObservanceIn();
234        }
235
236        /**
237         * Gets the observance information of a date.
238         * @param year the year
239         * @param month the month (1-12)
240         * @param day the day of the month
241         * @param hour the hour
242         * @param minute the minute
243         * @param second the second
244         * @return the observance information or null if none was found
245         */
246        private Boundary getObservanceBoundary(int year, int month, int day, int hour, int minute, int second) {
247                List<Observance> observances = getSortedObservances();
248                if (observances.isEmpty()) {
249                        return null;
250                }
251
252                DateValue givenTime = new DateTimeValueImpl(year, month, day, hour, minute, second);
253                int closestIndex = -1;
254                Observance closest = null;
255                DateTimeValue closestValue = null;
256                for (int i = 0; i < observances.size(); i++) {
257                        Observance observance = observances.get(i);
258
259                        //skip observances that start after the given time
260                        DateStart dtstart = observance.getDateStart();
261                        if (dtstart != null) {
262                                DateTimeValue dtstartValue = convert(dtstart);
263                                if (dtstartValue != null && dtstartValue.compareTo(givenTime) > 0) {
264                                        continue;
265                                }
266                        }
267
268                        RecurrenceIterator it = createIterator(observance);
269                        DateTimeValue prev = null;
270                        while (it.hasNext()) {
271                                DateTimeValue cur = (DateTimeValue) it.next();
272                                if (givenTime.compareTo(cur) < 0) {
273                                        //break if we have passed the given time
274                                        break;
275                                }
276
277                                prev = cur;
278                        }
279
280                        if (prev != null && (closestValue == null || closestValue.compareTo(prev) < 0)) {
281                                closestValue = prev;
282                                closest = observance;
283                                closestIndex = i;
284                        }
285                }
286
287                Observance observanceIn = closest;
288                DateTimeValue observanceInStart = closestValue;
289                Observance observanceAfter = null;
290                DateTimeValue observanceAfterStart = null;
291                if (closestIndex < observances.size() - 1) {
292                        observanceAfter = observances.get(closestIndex + 1);
293
294                        RecurrenceIterator it = createIterator(observanceAfter);
295                        while (it.hasNext()) {
296                                DateTimeValue cur = (DateTimeValue) it.next();
297                                if (givenTime.compareTo(cur) < 0) {
298                                        observanceAfterStart = cur;
299                                        break;
300                                }
301                        }
302                }
303
304                return new Boundary(observanceInStart, observanceIn, observanceAfterStart, observanceAfter);
305        }
306
307        private boolean hasDateStart(Observance observance) {
308                DateStart dtstart = observance.getDateStart();
309                return dtstart != null && dtstart.getValue() != null;
310        }
311
312        private boolean hasTimezoneOffsetFrom(Observance observance) {
313                UtcOffsetProperty offset = observance.getTimezoneOffsetFrom();
314                return offset != null && offset.getValue() != null;
315        }
316
317        private boolean hasTimezoneOffsetTo(Observance observance) {
318                UtcOffsetProperty offset = observance.getTimezoneOffsetTo();
319                return offset != null && offset.getValue() != null;
320        }
321
322        /**
323         * Gets all observances sorted by {@link DateStart}.
324         * @return the sorted observances
325         */
326        List<Observance> getSortedObservances() {
327                List<Observance> observances = new ArrayList<Observance>();
328                observances.addAll(component.getStandardTimes());
329                observances.addAll(component.getDaylightSavingsTime());
330
331                Collections.sort(observances, new Comparator<Observance>() {
332                        public int compare(Observance left, Observance right) {
333                                ICalDate startLeft = getValue(left.getDateStart());
334                                ICalDate startRight = getValue(right.getDateStart());
335                                if (startLeft == null && startRight == null) {
336                                        return 0;
337                                }
338                                if (startLeft == null) {
339                                        return -1;
340                                }
341                                if (startRight == null) {
342                                        return 1;
343                                }
344
345                                return startLeft.getRawComponents().compareTo(startRight.getRawComponents());
346                        }
347                });
348
349                return observances;
350        }
351
352        /**
353         * Creates an iterator which iterates over each of the dates in an
354         * observance.
355         * @param observance the observance
356         * @return the iterator
357         */
358        RecurrenceIterator createIterator(Observance observance) {
359                List<RecurrenceIterator> inclusions = new ArrayList<RecurrenceIterator>();
360                List<RecurrenceIterator> exclusions = new ArrayList<RecurrenceIterator>();
361
362                DateStart dtstart = observance.getDateStart();
363                if (dtstart != null) {
364                        DateValue dtstartValue = convert(dtstart);
365                        if (dtstartValue != null) {
366                                //add DTSTART property
367                                inclusions.add(new DateValueRecurrenceIterator(Arrays.asList(dtstartValue)));
368
369                                TimeZone utc = TimeZone.getTimeZone("UTC");
370
371                                //add RRULE properties
372                                for (RecurrenceRule rrule : observance.getProperties(RecurrenceRule.class)) {
373                                        Recurrence recur = rrule.getValue();
374                                        if (recur != null) {
375                                                RRule rruleValue = convert(recur);
376                                                inclusions.add(RecurrenceIteratorFactory.createRecurrenceIterator(rruleValue, dtstartValue, utc));
377                                        }
378                                }
379
380                                //add EXRULE properties
381                                for (ExceptionRule exrule : observance.getProperties(ExceptionRule.class)) {
382                                        Recurrence recur = exrule.getValue();
383                                        if (recur != null) {
384                                                RRule exruleValue = convert(recur);
385                                                exclusions.add(RecurrenceIteratorFactory.createRecurrenceIterator(exruleValue, dtstartValue, utc));
386                                        }
387                                }
388                        }
389                }
390
391                //add RDATE properties
392                List<ICalDate> rdates = new ArrayList<ICalDate>();
393                for (RecurrenceDates rdate : observance.getRecurrenceDates()) {
394                        rdates.addAll(rdate.getDates());
395                }
396                Collections.sort(rdates);
397                inclusions.add(new DateRecurrenceIterator(rdates));
398
399                //add EXDATE properties
400                List<ICalDate> exdates = new ArrayList<ICalDate>();
401                for (ExceptionDates exdate : observance.getProperties(ExceptionDates.class)) {
402                        exdates.addAll(exdate.getValues());
403                }
404                Collections.sort(exdates);
405                exclusions.add(new DateRecurrenceIterator(exdates));
406
407                RecurrenceIterator included = join(inclusions);
408                if (exclusions.isEmpty()) {
409                        return included;
410                }
411
412                RecurrenceIterator excluded = join(exclusions);
413                return RecurrenceIteratorFactory.except(included, excluded);
414        }
415
416        private RecurrenceIterator join(List<RecurrenceIterator> iterators) {
417                if (iterators.isEmpty()) {
418                        return new EmptyRecurrenceIterator();
419                }
420
421                RecurrenceIterator first = iterators.get(0);
422                if (iterators.size() == 1) {
423                        return first;
424                }
425
426                List<RecurrenceIterator> theRest = iterators.subList(1, iterators.size());
427                return RecurrenceIteratorFactory.join(first, theRest.toArray(new RecurrenceIterator[0]));
428        }
429
430        /**
431         * A recurrence iterator that doesn't have any elements.
432         */
433        private static class EmptyRecurrenceIterator implements RecurrenceIterator {
434                public boolean hasNext() {
435                        return false;
436                }
437
438                public DateValue next() {
439                        throw new NoSuchElementException();
440                }
441
442                public void advanceTo(DateValue newStartUtc) {
443                        //empty
444                }
445
446                public void remove() {
447                        //empty
448                }
449        }
450
451        /**
452         * A recurrence iterator that takes a collection of {@link DateValue}
453         * objects.
454         */
455        private static class DateValueRecurrenceIterator extends IteratorWrapper<DateValue> {
456                public DateValueRecurrenceIterator(Collection<DateValue> dates) {
457                        super(dates.iterator());
458                }
459
460                public DateValue next() {
461                        return it.next();
462                }
463        }
464
465        /**
466         * A recurrence iterator that takes a collection of {@link ICalDate}
467         * objects.
468         */
469        private static class DateRecurrenceIterator extends IteratorWrapper<ICalDate> {
470                public DateRecurrenceIterator(Collection<ICalDate> dates) {
471                        super(dates.iterator());
472                }
473
474                public DateValue next() {
475                        ICalDate value = it.next();
476                        return convert(value);
477                }
478        }
479
480        /**
481         * A recurrence iterator that wraps an {@link Iterator}.
482         */
483        private static abstract class IteratorWrapper<T> implements RecurrenceIterator {
484                protected final Iterator<T> it;
485
486                public IteratorWrapper(Iterator<T> it) {
487                        this.it = it;
488                }
489
490                public boolean hasNext() {
491                        return it.hasNext();
492                }
493
494                public void advanceTo(DateValue newStartUtc) {
495                        throw new UnsupportedOperationException();
496                }
497
498                public void remove() {
499                        it.remove();
500                }
501        }
502
503        /**
504         * Holds the timezone observance information of a particular date.
505         */
506        public static class Boundary {
507                private final DateTimeValue observanceInStart, observanceAfterStart;
508                private final Observance observanceIn, observanceAfter;
509
510                public Boundary(DateTimeValue observanceInStart, Observance observanceIn, DateTimeValue observanceAfterStart, Observance observanceAfter) {
511                        this.observanceInStart = observanceInStart;
512                        this.observanceAfterStart = observanceAfterStart;
513                        this.observanceIn = observanceIn;
514                        this.observanceAfter = observanceAfter;
515                }
516
517                /**
518                 * Gets start time of the observance that the date resides in.
519                 * @return the time
520                 */
521                public DateTimeValue getObservanceInStart() {
522                        return observanceInStart;
523                }
524
525                /**
526                 * Gets the start time the observance that comes after the observance
527                 * that the date resides in.
528                 * @return the time
529                 */
530                public DateTimeValue getObservanceAfterStart() {
531                        return observanceAfterStart;
532                }
533
534                /**
535                 * Gets the observance that the date resides in.
536                 * @return the observance
537                 */
538                public Observance getObservanceIn() {
539                        return observanceIn;
540                }
541
542                /**
543                 * Gets the observance that comes after the observance that the date
544                 * resides in.
545                 * @return the observance
546                 */
547                public Observance getObservanceAfter() {
548                        return observanceAfter;
549                }
550        }
551}