com.outerspacecat.icalendar.Schedule.java Source code

Java tutorial

Introduction

Here is the source code for com.outerspacecat.icalendar.Schedule.java

Source

/**
 * Copyright 2011 Caleb Richardson
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.outerspacecat.icalendar;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.zone.ZoneRulesException;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;

/**
 * A representation of a schedule.
 * <p>
 * Based on iCalendar concepts defined in <a
 * href="http://tools.ietf.org/html/rfc5545">RFC 5545</a>.
 * 
 * @author Caleb Richardson
 */
@Immutable
@ThreadSafe
public final class Schedule implements Serializable {
    private final static long serialVersionUID = 1L;

    private final TypedProperty<LocalDate> startDate;
    private final TypedProperty<LocalDateTime> startDateTime;
    private final TypedProperty<ZonedDateTime> zonedStartDateTime;
    private final TypedProperty<LocalDate> endDate;
    private final TypedProperty<LocalDateTime> endDateTime;
    private final TypedProperty<ZonedDateTime> zonedEndDateTime;
    private final TypedProperty<DurationType> duration;

    private Schedule(final TypedProperty<LocalDate> startDate, final TypedProperty<LocalDateTime> startDateTime,
            final TypedProperty<ZonedDateTime> zonedStartDateTime, final TypedProperty<LocalDate> endDate,
            final TypedProperty<LocalDateTime> endDateTime, final TypedProperty<ZonedDateTime> zonedEndDateTime,
            final TypedProperty<DurationType> duration) {
        this.startDate = startDate;
        this.startDateTime = startDateTime;
        this.zonedStartDateTime = zonedStartDateTime;
        this.endDate = endDate;
        this.endDateTime = endDateTime;
        this.zonedEndDateTime = zonedEndDateTime;
        this.duration = duration;
    }

    /**
     * Creates a new schedule.
     * 
     * @param startDate the start date of the schedule. Must be non {@code null}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromDate(final TypedProperty<LocalDate> startDate) {
        Preconditions.checkNotNull(startDate, "startDate required");

        return new Schedule(startDate, null, null, null, null, null, null);
    }

    /**
     * Creates a new schedule. The end date must be on or after the start date.
     * 
     * @param startDate the start date of the schedule. Must be non {@code null} .
     * @param endDate the end date of the schedule. Must be non {@code null}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromDates(final TypedProperty<LocalDate> startDate,
            final TypedProperty<LocalDate> endDate) {
        Preconditions.checkNotNull(startDate, "startDate required");
        Preconditions.checkNotNull(endDate, "endDate required");
        Preconditions.checkArgument(!endDate.getValue().isBefore(startDate.getValue()),
                "end date must be on or after start date");

        return new Schedule(startDate, null, null, endDate, null, null, null);
    }

    /**
     * Creates a new schedule.
     * 
     * @param startDate the start date of the schedule. Must be non {@code null} .
     * @param duration the duration of the schedule. Must be non {@code null}.
     *        Must return {@code false} for {@link DurationType#isNegative()} and
     *        {@code true} for {@link DurationType#isDayOrWeekOnly()}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromDateAndDuration(final TypedProperty<LocalDate> startDate,
            final TypedProperty<DurationType> duration) {
        Preconditions.checkNotNull(startDate, "startDate required");
        Preconditions.checkNotNull(duration, "duration required");
        Preconditions.checkArgument(!duration.getValue().isNegative(), "duration must be non negative");
        Preconditions.checkArgument(duration.getValue().isDayOrWeekOnly(), "duration must be day or week only");

        return new Schedule(startDate, null, null, null, null, null, duration);
    }

    /**
     * Creates a new schedule.
     * 
     * @param startDateTime the start date-time of the schedule. Must be non
     *        {@code null}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromDateTime(final TypedProperty<LocalDateTime> startDateTime) {
        Preconditions.checkNotNull(startDateTime, "startDateTime required");

        return new Schedule(null, startDateTime, null, null, null, null, null);
    }

    /**
     * Creates a new schedule. The end date-time must be on or after the start
     * date-time.
     * 
     * @param startDateTime the start date-time of the schedule. Must be non
     *        {@code null}.
     * @param endDateTime the end date-time of the schedule. Must be non
     *        {@code null}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromDateTimes(final TypedProperty<LocalDateTime> startDateTime,
            final TypedProperty<LocalDateTime> endDateTime) {
        Preconditions.checkNotNull(startDateTime, "startDateTime required");
        Preconditions.checkNotNull(endDateTime, "endDateTime required");
        Preconditions.checkArgument(!endDateTime.getValue().isBefore(startDateTime.getValue()),
                "end date-time must be on or after start date-time");

        return new Schedule(null, startDateTime, null, null, startDateTime, null, null);
    }

    /**
     * Creates a new schedule.
     * 
     * @param startDateTime the start date-time of the schedule. Must be non
     *        {@code null}.
     * @param duration the duration of the schedule. Must be non {@code null}.
     *        Must return {@code false} for {@link DurationType#isNegative()}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromDateTimeAndDuration(final TypedProperty<LocalDateTime> startDateTime,
            final TypedProperty<DurationType> duration) {
        Preconditions.checkNotNull(startDateTime, "startDateTime required");
        Preconditions.checkNotNull(duration, "duration required");
        Preconditions.checkArgument(!duration.getValue().isNegative(), "duration must be non negative");

        return new Schedule(null, startDateTime, null, null, null, null, duration);
    }

    /**
     * Creates a new schedule.
     * 
     * @param zonedStartDateTime the zoned start date-time of the schedule. Must
     *        be non {@code null}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromZonedDateTime(final TypedProperty<ZonedDateTime> zonedStartDateTime) {
        Preconditions.checkNotNull(zonedStartDateTime, "zonedStartDateTime required");

        return new Schedule(null, null, zonedStartDateTime, null, null, null, null);
    }

    /**
     * Creates a new schedule. The end instant must be on or after the start
     * instant.
     * 
     * @param zonedStartDateTime the zoned start date-time of the schedule. Must
     *        be non {@code null}.
     * @param zonedEndDateTime the zoned end date-time of the schedule. Must be
     *        non {@code null}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromZonedDateTimes(final TypedProperty<ZonedDateTime> zonedStartDateTime,
            final TypedProperty<ZonedDateTime> zonedEndDateTime) {
        Preconditions.checkNotNull(zonedStartDateTime, "zonedStartDateTime required");
        Preconditions.checkNotNull(zonedEndDateTime, "zonedEndDateTime required");
        Preconditions.checkArgument(!zonedEndDateTime.getValue().isBefore(zonedStartDateTime.getValue()),
                "end must be on or after start");

        return new Schedule(null, null, zonedStartDateTime, null, null, zonedEndDateTime, null);
    }

    /**
     * Creates a new schedule.
     * 
     * @param zonedStartDateTime the zoned start date-time of the schedule. Must
     *        be non {@code null}.
     * @param duration the duration of the schedule. Must be non {@code null}.
     *        Must return {@code false} for {@link DurationType#isNegative()}.
     * @return a new schedule. Never {@code null}.
     */
    public static Schedule fromZonedDateTimeAndDuration(final TypedProperty<ZonedDateTime> zonedStartDateTime,
            final TypedProperty<DurationType> duration) {
        Preconditions.checkNotNull(zonedStartDateTime, "zonedStartDateTime required");
        Preconditions.checkNotNull(duration, "duration required");
        Preconditions.checkArgument(!duration.getValue().isNegative(), "duration must be non negative");

        return new Schedule(null, null, zonedStartDateTime, null, null, null, duration);
    }

    /**
     * Parses a schedule.
     * 
     * @param startProperty the DTSTART property. Must be non {@code null}.
     * @param endProperty the DTEND property. May be {@code null}.
     * @param durationProperty the DURATION property. May be {@code null}.
     * @return a schedule. Never {@code null}.
     * @throws CalendarParseException if a valid schedule cannot be parsed
     */
    public static Schedule parse(final Property startProperty, final Property endProperty,
            final Property durationProperty) throws CalendarParseException {
        Preconditions.checkNotNull(startProperty, "start required");
        Preconditions.checkArgument(startProperty.getName().getName().equals("DTSTART"),
                "invalid start property name: " + startProperty.getName().getName());
        if (endProperty != null) {
            Preconditions.checkArgument(endProperty.getName().getName().equals("DTEND"),
                    "invalid end property name: " + endProperty.getName().getName());
        }
        if (durationProperty != null) {
            Preconditions.checkArgument(durationProperty.getName().getName().equals("DURATION"),
                    "invalid duration property name: " + durationProperty.getName().getName());
        }

        LocalDate startDate = null;
        ParsedDateTime startDateTime = null;
        ZoneId startTimeZone = null;
        ImmutableMap<String, Parameter> startParameters = null;
        LocalDate endDate = null;
        ParsedDateTime endDateTime = null;
        ZoneId endTimeZone = null;
        ImmutableMap<String, Parameter> endParameters = null;
        DurationType duration = null;
        ImmutableMap<String, Parameter> durationParameters = null;

        String startValue = startProperty.getParameterValue("VALUE").getValue();
        if (startValue.equals("DATE")) {
            startDate = startProperty.asDate();
        } else if (startValue.equals("DATE-TIME")) {
            startDateTime = startProperty.asDateTime();

            String startTzId = startProperty.getParameterValue("TZID").getValue();
            if (startTzId != null) {
                try {
                    startTimeZone = ZoneId.of(startTzId);
                } catch (ZoneRulesException e) {
                    throw new CalendarParseException("no time zone for DTSTART TZID: " + startTzId, e);
                }
            }

            if (startTimeZone == null) {
                if (startDateTime.isUtc())
                    startTimeZone = ZoneOffset.UTC;
            } else {
                if (startDateTime.isUtc())
                    throw new CalendarParseException("DTSTART specified as UTC with TZID");
            }
        } else {
            throw new CalendarParseException("invalid DSTART VALUE parameter: " + startValue);
        }

        startParameters = startProperty.getParametersExcept(ImmutableSet.of("VALUE", "TZID"));

        if (endProperty != null) {
            if (durationProperty != null)
                throw new CalendarParseException("both DTEND and DURATION specified");

            String endValue = endProperty.getParameterValue("VALUE").getValue();
            if (endValue.equals("DATE")) {
                if (startDate == null) {
                    throw new CalendarParseException("DATE-TIME DSTART specified with DATE DTEND");
                }

                endDate = endProperty.asDate();
            } else if (endValue.equals("DATE-TIME")) {
                if (startDateTime == null) {
                    throw new CalendarParseException("DATE DSTART specified with DATE-TIME DTEND");
                }

                endDateTime = endProperty.asDateTime();

                String endTzId = endProperty.getParameterValue("TZID").getValue();
                if (endTzId != null) {
                    try {
                        endTimeZone = ZoneId.of(endTzId);
                    } catch (ZoneRulesException e) {
                        throw new CalendarParseException("no time zone for DTEND TZID: " + endTzId, e);
                    }
                }

                if (endTimeZone == null) {
                    if (endDateTime.isUtc())
                        endTimeZone = ZoneOffset.UTC;
                } else {
                    if (endDateTime.isUtc())
                        throw new CalendarParseException("DTEND specified as UTC with TZID");
                }

                if (endTimeZone == null) {
                    if (startTimeZone != null) {
                        throw new CalendarParseException("Floating DTEND paired with non-floating DTSTART");
                    }
                } else {
                    if (startTimeZone == null) {
                        throw new CalendarParseException("Non-float DTEND paired with floating DTSTART");
                    }
                }
            }

            endParameters = endProperty.getParametersExcept(ImmutableSet.of("VALUE", "TZID"));
        }

        if (durationProperty != null) {
            if (endProperty != null)
                throw new CalendarParseException("both DTEND and DURATION specified");

            duration = durationProperty.asDuration();

            if (startDate != null) {
                if (!duration.isDayOrWeekOnly()) {
                    throw new CalendarParseException("DATE DTSTART can only be paired with a day or week DURATION");
                }
            }

            durationParameters = durationProperty.getParametersExcept(ImmutableSet.of());
        }

        if (startDate != null) {
            if (endDate != null) {
                return Schedule.fromDates(new TypedProperty<LocalDate>(startDate, startParameters.values()),
                        new TypedProperty<LocalDate>(endDate, endParameters.values()));
            } else if (duration != null) {
                return Schedule.fromDateAndDuration(
                        new TypedProperty<LocalDate>(startDate, startParameters.values()),
                        new TypedProperty<DurationType>(duration, durationParameters.values()));
            } else {
                return Schedule.fromDate(new TypedProperty<LocalDate>(startDate, startParameters.values()));
            }
        }

        if (startDateTime != null) {
            if (endDateTime != null) {
                if (startTimeZone != null) {
                    return Schedule.fromZonedDateTimes(
                            new TypedProperty<ZonedDateTime>(startDateTime.getDateTime().atZone(startTimeZone),
                                    startParameters.values()),
                            new TypedProperty<ZonedDateTime>(endDateTime.getDateTime().atZone(endTimeZone),
                                    endParameters.values()));
                } else {
                    return Schedule.fromDateTimes(
                            new TypedProperty<LocalDateTime>(startDateTime.getDateTime(), startParameters.values()),
                            new TypedProperty<LocalDateTime>(endDateTime.getDateTime(), endParameters.values()));
                }
            } else if (duration != null) {
                if (startTimeZone != null) {
                    return Schedule.fromZonedDateTimeAndDuration(
                            new TypedProperty<ZonedDateTime>(startDateTime.getDateTime().atZone(startTimeZone),
                                    startParameters.values()),
                            new TypedProperty<DurationType>(duration, durationParameters.values()));
                } else {
                    return Schedule.fromDateTimeAndDuration(
                            new TypedProperty<LocalDateTime>(startDateTime.getDateTime(), startParameters.values()),
                            new TypedProperty<DurationType>(duration, durationParameters.values()));
                }
            } else {
                return Schedule.fromDateTime(
                        new TypedProperty<LocalDateTime>(startDateTime.getDateTime(), startParameters.values()));
            }
        }

        throw new IllegalStateException("unexpected combination, startDate=" + startDate + ", startDateTime="
                + startDateTime + ", startTimeZone=" + startTimeZone + ", endDate=" + endDate + ", endDateTime="
                + endDateTime + ", endTimeZone=" + endTimeZone + ", duration=" + duration);
    }

    /**
     * Returns the start date of this schedule.
     * 
     * @return the start date of this schedule. May be {@code null}.
     */
    public TypedProperty<LocalDate> getStartDate() {
        return startDate;
    }

    /**
     * Returns the start date-time of this schedule.
     * 
     * @return the start date-time of this schedule. May be {@code null}.
     */
    public TypedProperty<LocalDateTime> getStartDateTime() {
        return startDateTime;
    }

    /**
     * Returns the zoned start date-time of this schedule.
     * 
     * @return the zoned start date-time of this schedule. May be {@code null}.
     */
    public TypedProperty<ZonedDateTime> getZonedStartDateTime() {
        return zonedStartDateTime;
    }

    /**
     * Returns the date of the start of this schedule.
     * 
     * @return the date of the start of this schedule. Never {@code null}
     */
    public LocalDate getDateOfStart() {
        if (getStartDate() != null) {
            return getStartDate().getValue();
        } else if (getStartDateTime() != null) {
            return getStartDateTime().getValue().toLocalDate();
        } else if (getZonedStartDateTime() != null) {
            return getZonedStartDateTime().getValue().toLocalDate();
        } else {
            throw new IllegalStateException("no start date or start date-time or zoned start date-time");
        }
    }

    /**
     * Returns the end date of this schedule.
     * 
     * @return the end date of this schedule. May be {@code null}.
     */
    public TypedProperty<LocalDate> getEndDate() {
        return endDate;
    }

    /**
     * Returns the end date-time of this schedule.
     * 
     * @return the end date-time of this schedule. May be {@code null}.
     */
    public TypedProperty<LocalDateTime> getEndDateTime() {
        return endDateTime;
    }

    /**
     * Returns the zoned end date-time of this schedule.
     * 
     * @return the zoned end date-time of this schedule. May be {@code null}.
     */
    public TypedProperty<ZonedDateTime> getZonedEndDateTime() {
        return zonedEndDateTime;
    }

    /**
     * Returns the duration of this schedule.
     * 
     * @return the duration of this schedule. May be {@code null}.
     */
    public TypedProperty<DurationType> getDuration() {
        return duration;
    }

    /**
     * Returns whether or not this schedule is floating.
     * 
     * @return whether or not this schedule is floating
     */
    public boolean isFloating() {
        return getStartDateTime() != null;
    }

    /**
     * Returns a {@link FixedSchedule} based on this schedule.
     * 
     * @param fallbackTimeZone a fallback time zone. Must be non {@code null}.
     * @return a {@link FixedSchedule} based on this schedule. Never {@code null}.
     */
    public FixedSchedule toFixed(final ZoneId fallbackTimeZone) {
        if (getStartDate() != null) {
            if (getEndDate() != null) {
                return new FixedSchedule(getStartDate().getValue(), getEndDate().getValue());
            } else if (getDuration() != null) {
                return new FixedSchedule(getStartDate().getValue(), getDuration().getValue());
            } else {
                return new FixedSchedule(getStartDate().getValue());
            }
        } else if (getStartDateTime() != null) {
            if (getEndDateTime() != null) {
                return new FixedSchedule(getStartDateTime().getValue().atZone(fallbackTimeZone),
                        getEndDateTime().getValue().atZone(fallbackTimeZone));
            } else if (getDuration() != null) {
                return new FixedSchedule(getStartDateTime().getValue().atZone(fallbackTimeZone),
                        getDuration().getValue());
            } else {
                return new FixedSchedule(getStartDateTime().getValue().atZone(fallbackTimeZone));
            }
        } else if (getZonedStartDateTime() != null) {
            if (getZonedEndDateTime() != null) {
                return new FixedSchedule(getZonedStartDateTime().getValue(), getZonedEndDateTime().getValue());
            } else if (getDuration() != null) {
                return new FixedSchedule(getZonedStartDateTime().getValue(), getDuration().getValue());
            } else {
                return new FixedSchedule(getZonedStartDateTime().getValue());
            }
        } else {
            throw new IllegalStateException("no start date or start date-time or zoned start date-time");
        }
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getStartDate(), getStartDateTime(), getZonedStartDateTime(), getEndDate(),
                getEndDateTime(), getZonedEndDateTime(), getDuration());
    }

    @Override
    public boolean equals(final Object obj) {
        return obj instanceof Schedule && Objects.equal(getStartDate(), ((Schedule) obj).getStartDate())
                && Objects.equal(getStartDateTime(), ((Schedule) obj).getStartDateTime())
                && Objects.equal(getZonedStartDateTime(), ((Schedule) obj).getZonedStartDateTime())
                && Objects.equal(getEndDate(), ((Schedule) obj).getEndDate())
                && Objects.equal(getEndDateTime(), ((Schedule) obj).getEndDateTime())
                && Objects.equal(getZonedEndDateTime(), ((Schedule) obj).getZonedEndDateTime())
                && Objects.equal(getDuration(), ((Schedule) obj).getDuration());
    }
}