/* * $Id: VEvent.java,v 1.36 2006/06/25 08:30:32 fortuna Exp $ [Apr 5, 2004] * * Copyright (c) 2004, Ben Fortuna * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * o Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * o Neither the name of Ben Fortuna nor the names of any other contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package net.fortuna.ical4j.model.component; import java.util.Iterator; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ComponentList; import net.fortuna.ical4j.model.Date; import net.fortuna.ical4j.model.DateList; import net.fortuna.ical4j.model.DateTime; import net.fortuna.ical4j.model.Dur; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.Period; import net.fortuna.ical4j.model.PeriodList; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.parameter.Value; import net.fortuna.ical4j.model.property.Clazz; import net.fortuna.ical4j.model.property.Created; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.DtStamp; import net.fortuna.ical4j.model.property.DtStart; import net.fortuna.ical4j.model.property.Duration; import net.fortuna.ical4j.model.property.ExDate; import net.fortuna.ical4j.model.property.ExRule; import net.fortuna.ical4j.model.property.Geo; import net.fortuna.ical4j.model.property.LastModified; import net.fortuna.ical4j.model.property.Location; import net.fortuna.ical4j.model.property.Organizer; import net.fortuna.ical4j.model.property.Priority; import net.fortuna.ical4j.model.property.RDate; import net.fortuna.ical4j.model.property.RRule; import net.fortuna.ical4j.model.property.RecurrenceId; import net.fortuna.ical4j.model.property.Sequence; import net.fortuna.ical4j.model.property.Status; import net.fortuna.ical4j.model.property.Summary; import net.fortuna.ical4j.model.property.Transp; import net.fortuna.ical4j.model.property.Uid; import net.fortuna.ical4j.model.property.Url; import net.fortuna.ical4j.util.CompatibilityHints; import net.fortuna.ical4j.util.Dates; import net.fortuna.ical4j.util.PropertyValidator; import net.fortuna.ical4j.util.Strings; /** * Defines an iCalendar VEVENT component. * * <pre> * 4.6.1 Event Component * * Component Name: "VEVENT" * * Purpose: Provide a grouping of component properties that describe an * event. * * Format Definition: A "VEVENT" calendar component is defined by the * following notation: * * eventc = "BEGIN" ":" "VEVENT" CRLF * eventprop *alarmc * "END" ":" "VEVENT" CRLF * * eventprop = *( * * ; the following are optional, * ; but MUST NOT occur more than once * * class / created / description / dtstart / geo / * last-mod / location / organizer / priority / * dtstamp / seq / status / summary / transp / * uid / url / recurid / * * ; either 'dtend' or 'duration' may appear in * ; a 'eventprop', but 'dtend' and 'duration' * ; MUST NOT occur in the same 'eventprop' * * dtend / duration / * * ; the following are optional, * ; and MAY occur more than once * * attach / attendee / categories / comment / * contact / exdate / exrule / rstatus / related / * resources / rdate / rrule / x-prop * * ) * </pre> * * Example 1 - Creating a new all-day event: * * <pre><code> * java.util.Calendar cal = java.util.Calendar.getInstance(); * cal.set(java.util.Calendar.MONTH, java.util.Calendar.DECEMBER); * cal.set(java.util.Calendar.DAY_OF_MONTH, 25); * * VEvent christmas = new VEvent(cal.getTime(), "Christmas Day"); * * // initialise as an all-day event.. * christmas.getProperties().getProperty(Property.DTSTART).getParameters().add(Value.DATE); * * // add timezone information.. * VTimeZone tz = VTimeZone.getDefault(); * TzId tzParam = new TzId(tz.getProperties().getProperty(Property.TZID).getValue()); * christmas.getProperties().getProperty(Property.DTSTART).getParameters().add(tzParam); * </code></pre> * * Example 2 - Creating an event of one (1) hour duration: * * <pre><code> * java.util.Calendar cal = java.util.Calendar.getInstance(); * // tomorrow.. * cal.add(java.util.Calendar.DAY_OF_MONTH, 1); * cal.set(java.util.Calendar.HOUR_OF_DAY, 9); * cal.set(java.util.Calendar.MINUTE, 30); * * VEvent meeting = new VEvent(cal.getTime(), 1000 * 60 * 60, "Progress Meeting"); * * // add timezone information.. * VTimeZone tz = VTimeZone.getDefault(); * TzId tzParam = new TzId(tz.getProperties().getProperty(Property.TZID).getValue()); * meeting.getProperties().getProperty(Property.DTSTART).getParameters().add(tzParam); * </code></pre> * * Example 3 - Retrieve a list of periods representing a recurring event in a * specified range: * * <pre><code> * Calendar weekday9AM = Calendar.getInstance(); * weekday9AM.set(2005, Calendar.MARCH, 7, 9, 0, 0); * weekday9AM.set(Calendar.MILLISECOND, 0); * * Calendar weekday5PM = Calendar.getInstance(); * weekday5PM.set(2005, Calendar.MARCH, 7, 17, 0, 0); * weekday5PM.set(Calendar.MILLISECOND, 0); * * // Do the recurrence until December 31st. * Calendar untilCal = Calendar.getInstance(); * untilCal.set(2005, Calendar.DECEMBER, 31); * untilCal.set(Calendar.MILLISECOND, 0); * * // 9:00AM to 5:00PM Rule * Recur recur = new Recur(Recur.WEEKLY, untilCal.getTime()); * recur.getDayList().add(WeekDay.MO); * recur.getDayList().add(WeekDay.TU); * recur.getDayList().add(WeekDay.WE); * recur.getDayList().add(WeekDay.TH); * recur.getDayList().add(WeekDay.FR); * recur.setInterval(3); * recur.setWeekStartDay(WeekDay.MO.getDay()); * RRule rrule = new RRule(recur); * * Summary summary = new Summary("TEST EVENTS THAT HAPPEN 9-5 MON-FRI"); * * weekdayNineToFiveEvents = new VEvent(); * weekdayNineToFiveEvents.getProperties().add(rrule); * weekdayNineToFiveEvents.getProperties().add(summary); * weekdayNineToFiveEvents.getProperties().add( * new DtStart(weekday9AM.getTime())); * weekdayNineToFiveEvents.getProperties().add( * new DtEnd(weekday5PM.getTime())); * * // Test Start 04/01/2005, End One month later. * // Query Calendar Start and End Dates. * Calendar queryStartDate = Calendar.getInstance(); * queryStartDate.set(2005, Calendar.APRIL, 1, 14, 47, 0); * queryStartDate.set(Calendar.MILLISECOND, 0); * Calendar queryEndDate = Calendar.getInstance(); * queryEndDate.set(2005, Calendar.MAY, 1, 11, 15, 0); * queryEndDate.set(Calendar.MILLISECOND, 0); * * // This range is monday to friday every three weeks, starting from * // March 7th 2005, which means for our query dates we need * // April 18th through to the 22nd. * PeriodList periods = * weekdayNineToFiveEvents.getPeriods(queryStartDate.getTime(), * queryEndDate.getTime()); * </code></pre> * * @author Ben Fortuna */ public class VEvent extends CalendarComponent { private static final long serialVersionUID = 2547948989200697335L; private ComponentList alarms; /** * Default constructor. */ public VEvent() { super(VEVENT); this.alarms = new ComponentList(); getProperties().add(new DtStamp()); } /** * Constructor. * * @param properties * a list of properties */ public VEvent(final PropertyList properties) { super(VEVENT, properties); this.alarms = new ComponentList(); } /** * Constructor. * * @param properties * a list of properties * @param alarms * a list of alarms */ public VEvent(final PropertyList properties, final ComponentList alarms) { super(VEVENT, properties); this.alarms = alarms; } /** * Constructs a new VEVENT instance starting at the specified * time with the specified summary. * @param start the start date of the new event * @param summary the event summary */ public VEvent(final Date start, final String summary) { this(); getProperties().add(new DtStart(start)); getProperties().add(new Summary(summary)); } /** * Constructs a new VEVENT instance starting and ending at the specified * times with the specified summary. * @param start the start date of the new event * @param end the end date of the new event * @param summary the event summary */ public VEvent(final Date start, final Date end, final String summary) { this(); getProperties().add(new DtStart(start)); getProperties().add(new DtEnd(end)); getProperties().add(new Summary(summary)); } /** * Constructs a new VEVENT instance starting at the specified * times, for the specified duration, with the specified summary. * @param start the start date of the new event * @param duration the duration of the new event * @param summary the event summary */ public VEvent(final Date start, final Dur duration, final String summary) { this(); getProperties().add(new DtStart(start)); getProperties().add(new Duration(duration)); getProperties().add(new Summary(summary)); } /** * Returns the list of alarms for this event. * @return a component list */ public final ComponentList getAlarms() { return alarms; } /** * @see java.lang.Object#toString() */ public final String toString() { StringBuffer b = new StringBuffer(); b.append(BEGIN); b.append(':'); b.append(getName()); b.append(Strings.LINE_SEPARATOR); b.append(getProperties()); b.append(getAlarms()); b.append(END); b.append(':'); b.append(getName()); b.append(Strings.LINE_SEPARATOR); return b.toString(); } /** * @see net.fortuna.ical4j.model.Component#validate(boolean) */ public final void validate(final boolean recurse) throws ValidationException { // validate that getAlarms() only contains VAlarm components Iterator iterator = getAlarms().iterator(); while (iterator.hasNext()) { Component component = (Component) iterator.next(); if (!(component instanceof VAlarm)) { throw new ValidationException( "Component [" + component.getName() + "] may not occur in VEVENT"); } } if (!CompatibilityHints.isHintEnabled( CompatibilityHints.KEY_RELAXED_VALIDATION)) { // From "4.8.4.7 Unique Identifier": // Conformance: The property MUST be specified in the "VEVENT", "VTODO", // "VJOURNAL" or "VFREEBUSY" calendar components. PropertyValidator.getInstance().assertOne(Property.UID, getProperties()); // From "4.8.7.2 Date/Time Stamp": // Conformance: This property MUST be included in the "VEVENT", "VTODO", // "VJOURNAL" or "VFREEBUSY" calendar components. PropertyValidator.getInstance().assertOne(Property.DTSTAMP, getProperties()); } /* * ; the following are optional, ; but MUST NOT occur more than once * * class / created / description / dtstart / geo / last-mod / location / * organizer / priority / dtstamp / seq / status / summary / transp / * uid / url / recurid / */ PropertyValidator.getInstance().assertOneOrLess(Property.CLASS, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.CREATED, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.DESCRIPTION, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.DTSTART, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.GEO, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.LAST_MODIFIED, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.LOCATION, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.ORGANIZER, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.PRIORITY, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.DTSTAMP, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.SEQUENCE, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.STATUS, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.SUMMARY, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.TRANSP, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.UID, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.URL, getProperties()); PropertyValidator.getInstance().assertOneOrLess(Property.RECURRENCE_ID, getProperties()); Status status = (Status) getProperty(Property.STATUS); if (status != null && !Status.VEVENT_TENTATIVE.equals(status) && !Status.VEVENT_CONFIRMED.equals(status) && !Status.VEVENT_CANCELLED.equals(status)) { throw new ValidationException( "Status property [" + status.toString() + "] is not applicable for VEVENT"); } /* * ; either 'dtend' or 'duration' may appear in ; a 'eventprop', but * 'dtend' and 'duration' ; MUST NOT occur in the same 'eventprop' * * dtend / duration / */ try { PropertyValidator.getInstance().assertNone(Property.DTEND, getProperties()); } catch (ValidationException ve) { PropertyValidator.getInstance().assertNone(Property.DURATION, getProperties()); } if (getProperty(Property.DTEND) != null) { /* * The "VEVENT" is also the calendar component used to specify an * anniversary or daily reminder within a calendar. These events have a * DATE value type for the "DTSTART" property instead of the default * data type of DATE-TIME. If such a "VEVENT" has a "DTEND" property, it * MUST be specified as a DATE value also. The anniversary type of * "VEVENT" can span more than one date (i.e, "DTEND" property value is * set to a calendar date after the "DTSTART" property value). */ DtStart start = (DtStart) getProperty(Property.DTSTART); DtEnd end = (DtEnd) getProperty(Property.DTEND); if (start != null) { Parameter value = start.getParameter(Parameter.VALUE); if (value != null && !value.equals(end.getParameter(Parameter.VALUE))) { throw new ValidationException("Property [" + Property.DTEND + "] must have the same [" + Parameter.VALUE + "] as [" + Property.DTSTART + "]"); } } } /* * ; the following are optional, ; and MAY occur more than once * * attach / attendee / categories / comment / contact / exdate / exrule / * rstatus / related / resources / rdate / rrule / x-prop */ if (recurse) { validateProperties(); } } /** * Returns a normalised list of periods representing the consumed time for this * event. * @param rangeStart * @param rangeEnd * @return a normalised list of periods representing consumed time for this event * @see VEvent#getConsumedTime(Date, Date, boolean) */ public final PeriodList getConsumedTime(final Date rangeStart, final Date rangeEnd) { return getConsumedTime(rangeStart, rangeEnd, true); } /** * Returns a list of periods representing the consumed time for this event * in the specified range. Note that the returned list may contain a single * period for non-recurring components or multiple periods for recurring * components. If no time is consumed by this event an empty list is returned. * @param rangeStart the start of the range to check for consumed time * @param rangeEnd the end of the range to check for consumed time * @param normalise indicate whether the returned list of periods should be * normalised * @return a list of periods representing consumed time for this event */ public final PeriodList getConsumedTime(final Date rangeStart, final Date rangeEnd, final boolean normalise) { PeriodList periods = new PeriodList(); // if component is transparent return empty list.. if (Transp.TRANSPARENT.equals(getProperty(Property.TRANSP))) { return periods; } DtStart start = (DtStart) getProperty(Property.DTSTART); DtEnd end = (DtEnd) getProperty(Property.DTEND); Duration duration = (Duration) getProperty(Property.DURATION); // if no start date or duration specified return empty list.. if (start == null || (duration == null && end == null)) { return periods; } // if an explicit event duration is not specified, derive a value for recurring // periods from the end date.. Dur rDuration; if (duration == null) { rDuration = new Dur(start.getDate(), end.getDate()); } else { rDuration = duration.getDuration(); } // adjust range start back by duration to allow for recurrences that // start before the range but finish inside.. // FIXME: See bug #1325558.. Date adjustedRangeStart = new DateTime(rangeStart); adjustedRangeStart.setTime(rDuration.negate().getTime(rangeStart).getTime()); // if start/end specified as anniversary-type (i.e. uses DATE values // rather than DATE-TIME), return empty list.. if (Value.DATE.equals(start.getParameter(Parameter.VALUE))) { return periods; } // recurrence dates.. PropertyList rDates = getProperties(Property.RDATE); for (Iterator i = rDates.iterator(); i.hasNext();) { RDate rdate = (RDate) i.next(); // only period-based rdates are applicable.. // FIXME: ^^^ not true - date-time/date also applicable.. if (Value.PERIOD.equals(rdate.getParameter(Parameter.VALUE))) { for (Iterator j = rdate.getPeriods().iterator(); j.hasNext();) { Period period = (Period) j.next(); if (period.getStart().before(rangeEnd) && period.getEnd().after(rangeStart)) { periods.add(period); } } } } // recurrence rules.. PropertyList rRules = getProperties(Property.RRULE); for (Iterator i = rRules.iterator(); i.hasNext();) { RRule rrule = (RRule) i.next(); DateList startDates = rrule.getRecur().getDates(start.getDate(), adjustedRangeStart, rangeEnd, (Value) start.getParameter(Parameter.VALUE)); // DateList startDates = rrule.getRecur().getDates(start.getDate(), rangeStart, rangeEnd, (Value) start.getParameters().getParameter(Parameter.VALUE)); for (int j = 0; j < startDates.size(); j++) { Date startDate = (Date) startDates.get(j); periods.add(new Period(new DateTime(startDate), rDuration)); } } // add first instance if included in range.. if (start.getDate().before(rangeEnd)) { if (end != null && end.getDate().after(rangeStart)) { periods.add(new Period(new DateTime(start.getDate()), new DateTime(end.getDate()))); } else if (duration != null) { Period period = new Period(new DateTime(start.getDate()), duration.getDuration()); if (period.getEnd().after(rangeStart)) { periods.add(period); } } } // exception dates.. PropertyList exDates = getProperties(Property.EXDATE); for (Iterator i = exDates.iterator(); i.hasNext();) { ExDate exDate = (ExDate) i.next(); for (Iterator j = periods.iterator(); j.hasNext();) { Period period = (Period) j.next(); // for DATE-TIME instances check for DATE-based exclusions also.. if (exDate.getDates().contains(period.getStart()) || exDate.getDates().contains(new Date(period.getStart()))) { j.remove(); } } } // exception rules.. // FIXME: exception rules should be consistent with exception dates (i.e. not use periods?).. PropertyList exRules = getProperties(Property.EXRULE); PeriodList exPeriods = new PeriodList(); for (Iterator i = exRules.iterator(); i.hasNext();) { ExRule exrule = (ExRule) i.next(); // DateList startDates = exrule.getRecur().getDates(start.getDate(), adjustedRangeStart, rangeEnd, (Value) start.getParameters().getParameter(Parameter.VALUE)); DateList startDates = exrule.getRecur().getDates(start.getDate(), rangeStart, rangeEnd, (Value) start.getParameter(Parameter.VALUE)); for (Iterator j = startDates.iterator(); j.hasNext();) { Date startDate = (Date) j.next(); exPeriods.add(new Period(new DateTime(startDate), rDuration)); } } // apply exceptions.. if (!exPeriods.isEmpty()) { periods = periods.subtract(exPeriods); } // if periods already specified through recurrence, return.. // ..also normalise before returning. if (!periods.isEmpty() && normalise) { return periods.normalise(); } return periods; } /** * @return the optional access classification property for an event */ public final Clazz getClassification() { return (Clazz) getProperty(Property.CLASS); } /** * @return the optional creation-time property for an event */ public final Created getCreated() { return (Created) getProperty(Property.CREATED); } /** * @return the optional description property for an event */ public final Description getDescription() { return (Description) getProperty(Property.DESCRIPTION); } /** * Convenience method to pull the DTSTART out of the property list. * @return The DtStart object representation of the start Date */ public final DtStart getStartDate() { return (DtStart) getProperty(Property.DTSTART); } /** * @return the optional geographic position property for an event */ public final Geo getGeographicPos() { return (Geo) getProperty(Property.GEO); } /** * @return the optional last-modified property for an event */ public final LastModified getLastModified() { return (LastModified) getProperty(Property.LAST_MODIFIED); } /** * @return the optional location property for an event */ public final Location getLocation() { return (Location) getProperty(Property.LOCATION); } /** * @return the optional organizer property for an event */ public final Organizer getOrganizer() { return (Organizer) getProperty(Property.ORGANIZER); } /** * @return the optional priority property for an event */ public final Priority getPriority() { return (Priority) getProperty(Property.PRIORITY); } /** * @return the optional date-stamp property */ public final DtStamp getDateStamp() { return (DtStamp) getProperty(Property.DTSTAMP); } /** * @return the optional sequence number property for an event */ public final Sequence getSequence() { return (Sequence) getProperty(Property.SEQUENCE); } /** * @return the optional status property for an event */ public final Status getStatus() { return (Status) getProperty(Property.STATUS); } /** * @return the optional summary property for an event */ public final Summary getSummary() { return (Summary) getProperty(Property.SUMMARY); } /** * @return the optional time transparency property for an event */ public final Transp getTransparency() { return (Transp) getProperty(Property.TRANSP); } /** * @return the optional URL property for an event */ public final Url getUrl() { return (Url) getProperty(Property.URL); } /** * @return the optional recurrence identifier property for an event */ public final RecurrenceId getRecurrenceId() { return (RecurrenceId) getProperty(Property.RECURRENCE_ID); } /** * Returns the end date of this event. Where an end date is not * available it will be derived from the event duration. * @return a DtEnd instance, or null if one cannot be derived */ public final DtEnd getEndDate() { return getEndDate(true); } /** * Convenience method to pull the DTEND out of the property list. If * DTEND was not specified, use the DTSTART + DURATION to calculate it. * @param deriveFromDuration specifies whether to derive an end date from * the event duration where an end date is not found * @return The end for this VEVENT. */ public final DtEnd getEndDate(final boolean deriveFromDuration) { DtEnd dtEnd = (DtEnd) getProperty(Property.DTEND); if (deriveFromDuration) { // No DTEND? No problem, we'll use the DURATION. if (dtEnd == null) { DtStart dtStart = getStartDate(); Duration vEventDuration = getDuration(); if (vEventDuration != null) { dtEnd = new DtEnd(Dates.getInstance(vEventDuration.getDuration().getTime(dtStart.getDate()), (Value) dtStart.getParameter(Parameter.VALUE))); if (dtStart.isUtc()) { dtEnd.setUtc(true); } } } } return dtEnd; } /** * @return the optional Duration property */ public final Duration getDuration() { return (Duration) getProperty(Property.DURATION); } /** * Returns the UID property of this component if available. * @return a Uid instance, or null if no UID property exists */ public final Uid getUid() { return (Uid) getProperty(Property.UID); } }