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}