org.silverpeas.core.calendar.CalendarEventOccurrenceGenerationTest.java Source code

Java tutorial

Introduction

Here is the source code for org.silverpeas.core.calendar.CalendarEventOccurrenceGenerationTest.java

Source

/*
 * Copyright (C) 2000 - 2018 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.silverpeas.core.calendar;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.silverpeas.core.admin.service.OrganizationController;
import org.silverpeas.core.admin.user.model.UserDetail;
import org.silverpeas.core.calendar.ical4j.ICal4JCalendarEventOccurrenceGenerator;
import org.silverpeas.core.calendar.ical4j.ICal4JDateCodec;
import org.silverpeas.core.calendar.ical4j.ICal4JRecurrenceCodec;
import org.silverpeas.core.calendar.repository.CalendarEventOccurrenceRepository;
import org.silverpeas.core.date.Period;
import org.silverpeas.core.date.TimeUnit;
import org.silverpeas.core.persistence.datasource.OperationContext;
import org.silverpeas.core.persistence.datasource.model.jpa.JpaPersistOperation;
import org.silverpeas.core.persistence.datasource.model.jpa.JpaUpdateOperation;
import org.silverpeas.core.test.extention.EnableSilverTestEnv;
import org.silverpeas.core.test.extention.TestManagedBeans;
import org.silverpeas.core.test.extention.TestManagedMock;

import java.time.LocalDate;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import java.util.stream.Collectors;

import static java.time.DayOfWeek.*;
import static java.time.Month.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.when;
import static org.silverpeas.core.date.TimeUnit.MONTH;
import static org.silverpeas.core.date.TimeUnit.WEEK;

/**
 * Unit tests on the generation of event occurrences between two given datetimes.
 * @author mmoquillon
 */
@EnableSilverTestEnv
@TestManagedBeans({ JpaPersistOperation.class, JpaUpdateOperation.class })
public class CalendarEventOccurrenceGenerationTest {

    private static final String EVENT_TITLE = "an event title";
    private static final String EVENT_DESCRIPTION = "a short event description";
    private static final String ATTR_TEST_ID = "TEST_EVENT_ID";
    private static final ZoneId PARIS_ZONE_ID = ZoneId.of("Europe/Paris");
    private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC");

    private CalendarEventOccurrenceGenerator generator = new ICal4JCalendarEventOccurrenceGenerator(
            new ICal4JDateCodec(), new ICal4JRecurrenceCodec(new ICal4JDateCodec()));

    @BeforeEach
    public void mockCalendarOccurrenceRepository(@TestManagedMock CalendarEventOccurrenceRepository repository,
            @TestManagedMock OrganizationController organizationController) {
        when(organizationController.getUserDetail(anyString())).thenAnswer(a -> {
            String id = a.getArgument(0);
            UserDetail user = new UserDetail();
            user.setId(id);
            return user;
        });
        when(repository.getAll(anyCollection(), any(Period.class))).thenReturn(Collections.emptyList());

        OperationContext.fromUser("0");
    }

    @Test
    public void nothingDoneWithAnEmptyListOfEvents() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(Collections.emptyList(),
                in(Year.of(2016)));
        assertThat(occurrences.isEmpty(), is(true));
    }

    @Test
    public void noOccurrencesIfNoEventInTheGivenPeriod() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(calendarEventsForTest(),
                in(YearMonth.of(2016, 1)));
        assertThat(occurrences.isEmpty(), is(true));
    }

    @Test
    public void countEventOccurrencesInYear() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(calendarEventsForTest(),
                in(Year.of(2016)));
        assertThat(occurrences.isEmpty(), is(false));
        assertThat(occurrences.size(), is(103));

        // compute the occurrence count both per month and per event
        int[] occurrenceCountPerMonth = new int[12];
        int[] occurrenceCountPerEvent = new int[6];
        occurrences.forEach(o -> {
            occurrenceCountPerMonth[o.getStartDate().get(ChronoField.MONTH_OF_YEAR) - 1] += 1;
            occurrenceCountPerEvent[Integer.parseInt(o.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get())
                    - 1] += 1;
        });
        // check now the count of occurrences per month is ok
        assertThat(occurrenceCountPerMonth[JANUARY.ordinal()], is(0));
        assertThat(occurrenceCountPerMonth[FEBRUARY.ordinal()], is(0));
        assertThat(occurrenceCountPerMonth[MARCH.ordinal()], is(4));
        assertThat(occurrenceCountPerMonth[APRIL.ordinal()], is(6));
        assertThat(occurrenceCountPerMonth[MAY.ordinal()], is(10));
        assertThat(occurrenceCountPerMonth[JUNE.ordinal()], is(10));
        assertThat(occurrenceCountPerMonth[JULY.ordinal()], is(4));
        assertThat(occurrenceCountPerMonth[AUGUST.ordinal()], is(5));
        assertThat(occurrenceCountPerMonth[SEPTEMBER.ordinal()], is(18));
        assertThat(occurrenceCountPerMonth[OCTOBER.ordinal()], is(17));
        assertThat(occurrenceCountPerMonth[NOVEMBER.ordinal()], is(17));
        assertThat(occurrenceCountPerMonth[DECEMBER.ordinal()], is(12));

        // check now the count of occurrences per event is ok
        assertThat(occurrenceCountPerEvent[Integer.parseInt("1") - 1], is(1));
        assertThat(occurrenceCountPerEvent[Integer.parseInt("2") - 1], is(1));
        assertThat(occurrenceCountPerEvent[Integer.parseInt("3") - 1], is(42));
        assertThat(occurrenceCountPerEvent[Integer.parseInt("4") - 1], is(1));
        assertThat(occurrenceCountPerEvent[Integer.parseInt("5") - 1], is(46));
        assertThat(occurrenceCountPerEvent[Integer.parseInt("6") - 1], is(12));
    }

    @Test
    public void countEventOccurrencesInMay() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(calendarEventsForTest(),
                in(YearMonth.of(2016, Month.MAY)));
        assertThat(occurrences.isEmpty(), is(false));
        assertThat(occurrences.size(), is(10));
        List<String> allEventIds = occurrences.stream()
                .map(o -> o.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get())
                .collect(Collectors.toList());
        assertThat(
                allEventIds.stream().distinct().allMatch(id -> id.equals("2") || id.equals("3") || id.equals("6")),
                is(true));
        assertThat(allEventIds.stream().filter(id -> id.equals("2")).count(), is(1L));
        assertThat(allEventIds.stream().filter(id -> id.equals("3")).count(), is(4L));
        assertThat(allEventIds.stream().filter(id -> id.equals("6")).count(), is(5L));
    }

    @Test
    public void countEventOccurrencesInJuly() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(calendarEventsForTest(),
                in(YearMonth.of(2016, Month.JULY)));
        assertThat(occurrences.isEmpty(), is(false));
        assertThat(occurrences.size(), is(4));
        List<String> allEventIds = occurrences.stream()
                .map(o -> o.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get())
                .collect(Collectors.toList());
        assertThat(allEventIds.stream().distinct().allMatch(id -> id.equals("3") || id.equals("4")), is(true));
        assertThat(allEventIds.stream().filter(id -> id.equals("3")).count(), is(3L));
        assertThat(allEventIds.stream().filter(id -> id.equals("4")).count(), is(1L));
    }

    @Test
    public void countEventOccurrencesInAGivenPeriod() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(calendarEventsForTest(),
                Period.between(date(2016, 8, 8), date(2016, 8, 14)));
        assertThat(occurrences.isEmpty(), is(false));
        assertThat(occurrences.size(), is(2));
        List<String> allEventIds = occurrences.stream()
                .map(o -> o.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get())
                .collect(Collectors.toList());
        assertThat(allEventIds.stream().distinct().allMatch(id -> id.equals("1") || id.equals("3")), is(true));
        assertThat(allEventIds.stream().filter(id -> id.equals("1")).count(), is(1L));
        assertThat(allEventIds.stream().filter(id -> id.equals("3")).count(), is(1L));
    }

    @Test
    public void dateOfEventOccurrencesInJuly() {
        List<CalendarEventOccurrence> occurrences = generator.generateOccurrencesOf(calendarEventsForTest(),
                in(YearMonth.of(2016, Month.JULY)));
        assertThat(occurrences.size(), is(4));
        // first occurrence
        Iterator<CalendarEventOccurrence> iterator = occurrences.iterator();
        CalendarEventOccurrence occurrence = iterator.next();
        assertThat(occurrence.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get(), is("3"));
        assertThat(occurrence.getStartDate(), is(dateTimeInUTC(2016, 7, 1, 9, 0)));
        assertThat(occurrence.getEndDate(), is(dateTimeInUTC(2016, 7, 1, 9, 15)));
        // second occurrence
        occurrence = iterator.next();
        assertThat(occurrence.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get(), is("3"));
        assertThat(occurrence.getStartDate(), is(dateTimeInUTC(2016, 7, 8, 9, 0)));
        assertThat(occurrence.getEndDate(), is(dateTimeInUTC(2016, 7, 8, 9, 15)));
        // third occurrence
        occurrence = iterator.next();
        assertThat(occurrence.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get(), is("4"));
        assertThat(occurrence.getStartDate(), is(date(2016, 7, 11)));
        assertThat(occurrence.getEndDate(), is(date(2016, 7, 22)));
        // fourth occurrence
        occurrence = iterator.next();
        assertThat(occurrence.getCalendarEvent().getAttributes().get(ATTR_TEST_ID).get(), is("3"));
        assertThat(occurrence.getStartDate(), is(dateTimeInUTC(2016, 7, 29, 9, 0)));
        assertThat(occurrence.getEndDate(), is(dateTimeInUTC(2016, 7, 29, 9, 15)));
    }

    @Test
    public void nextOccurrenceAboutNonRecurrentOneDayEventShouldWork() {
        CalendarEvent event = calendarEventForTest(Period.between(date(2017, 12, 12), date(2017, 12, 12)));
        ZonedDateTime from = ZonedDateTime.parse("2017-12-12T00:00:00+01:00");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());

        from = ZonedDateTime.parse("2017-12-12T00:00:00-01:00");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());

        from = ZonedDateTime.parse("2017-12-11T23:59:59+10:00");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 13)));

        from = ZonedDateTime.parse("2017-12-11T23:59:59Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 13)));

        from = ZonedDateTime.parse("2017-12-12T00:00:00Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutNonRecurrentSeveralDayEventShouldWork() {
        CalendarEvent event = calendarEventForTest(Period.between(date(2017, 12, 12), date(2017, 12, 15)));
        ZonedDateTime from = ZonedDateTime.parse("2017-12-12T00:00:00+01:00");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());

        from = ZonedDateTime.parse("2017-12-12T00:00:00-01:00");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());

        from = ZonedDateTime.parse("2017-12-11T23:59:59+12:00");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 15)));

        from = ZonedDateTime.parse("2017-12-11T23:59:59Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 15)));

        from = ZonedDateTime.parse("2017-12-12T00:00:00Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutNonRecurrentHourEventOnOneDayShouldWork() {
        CalendarEvent event = calendarEventForTest(
                Period.between(dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 12, 14, 45)));
        ZonedDateTime from = ZonedDateTime.parse("2017-12-12T13:30:00+01:00");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 12, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:29:59Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 12, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:30:00Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutNonRecurrentHugeHourEventOnOneDayShouldWork() {
        CalendarEvent event = calendarEventForTest(
                Period.between(dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 15, 14, 45)));
        ZonedDateTime from = ZonedDateTime.parse("2017-12-12T13:30:00+01:00[Europe/Paris]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 15, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:29:59Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 15, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:30:00Z");
        result = generator.generateNextOccurrenceOf(event, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentOneDayEventShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(Period.between(date(2017, 12, 12), date(2017, 12, 12)))
                .recur(Recurrence.every(1, TimeUnit.DAY).until(3));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 13)));

        from = ZonedDateTime.parse("2017-12-11T23:59:59Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 13)));

        from = ZonedDateTime.parse("2017-12-12T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 13)));
        assertThat(result.getEndDate(), is(date(2017, 12, 14)));

        from = ZonedDateTime.parse("2017-12-13T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 14)));
        assertThat(result.getEndDate(), is(date(2017, 12, 15)));

        from = ZonedDateTime.parse("2017-12-14T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentOneDayEventWithExceptionShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(Period.between(date(2017, 12, 12), date(2017, 12, 12)))
                .recur(Recurrence.every(1, TimeUnit.DAY).until(3)
                        .excludeEventOccurrencesStartingAt(date(2017, 12, 12), date(2017, 12, 14)));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 13)));
        assertThat(result.getEndDate(), is(date(2017, 12, 14)));

        from = ZonedDateTime.parse("2017-12-13T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentSeveralDayEventShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(Period.between(date(2017, 12, 12), date(2017, 12, 15)))
                .recur(Recurrence.every(1, TimeUnit.DAY).until(3));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 15)));

        from = ZonedDateTime.parse("2017-12-11T23:59:59Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 12)));
        assertThat(result.getEndDate(), is(date(2017, 12, 15)));

        from = ZonedDateTime.parse("2017-12-12T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 13)));
        assertThat(result.getEndDate(), is(date(2017, 12, 16)));

        from = ZonedDateTime.parse("2017-12-13T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 14)));
        assertThat(result.getEndDate(), is(date(2017, 12, 17)));

        from = ZonedDateTime.parse("2017-12-14T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentSeveralDayEventWithExceptionShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(Period.between(date(2017, 12, 12), date(2017, 12, 15)))
                .recur(Recurrence.every(1, TimeUnit.DAY).until(3)
                        .excludeEventOccurrencesStartingAt(date(2017, 12, 12), date(2017, 12, 14)));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(date(2017, 12, 13)));
        assertThat(result.getEndDate(), is(date(2017, 12, 16)));

        from = ZonedDateTime.parse("2017-12-13T00:00:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentHourEventShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(
                Period.between(dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 12, 14, 45)))
                        .recur(Recurrence.every(1, TimeUnit.DAY).until(3));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 12, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:29:59Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 12, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 13, 14, 45)));

        from = ZonedDateTime.parse("2017-12-13T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 14, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 14, 14, 45)));

        from = ZonedDateTime.parse("2017-12-14T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentHourEventStartingOnSummerShouldWork() {
        final OffsetDateTime startDateTimeOnParis = dateTimeOnParis(2017, 7, 11, 23, 0);
        final OffsetDateTime endDateTimeOnParis = dateTimeOnParis(2017, 7, 12, 0, 45);
        assertThat(startDateTimeOnParis.withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 7, 11, 21, 0)));
        assertThat(endDateTimeOnParis.withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 7, 11, 22, 45)));
        assertThat(dateTimeOnParis(2017, 12, 11, 23, 0).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 12, 11, 22, 0)));
        assertThat(dateTimeOnParis(2017, 12, 12, 0, 45).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 12, 11, 23, 45)));
        CalendarEvent recurrentEvent = calendarEventForTest(
                Period.between(startDateTimeOnParis, endDateTimeOnParis), PARIS_ZONE_ID)
                        .recur(Recurrence.every(1, TimeUnit.MONTH).until(10));
        ZonedDateTime from = ZonedDateTime.parse("2017-12-11T21:59:59-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 1, 11, 22, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 1, 11, 23, 45)));

        from = ZonedDateTime.parse("2017-12-11T22:00:00-01:00[Atlantic/Azores]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 1, 11, 22, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 1, 11, 23, 45)));

        from = ZonedDateTime.parse("2017-12-11T22:59:59+00:00[UTC]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 1, 11, 22, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 1, 11, 23, 45)));

        from = ZonedDateTime.parse("2017-12-11T23:00:00+00:00[UTC]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 1, 11, 22, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 1, 11, 23, 45)));

        from = ZonedDateTime.parse("2017-12-11T22:59:59+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 11, 22, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 11, 23, 45)));

        from = ZonedDateTime.parse("2017-12-11T23:00:00+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 1, 11, 22, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 1, 11, 23, 45)));
    }

    @Test
    public void nextOccurrenceAboutRecurrentHourEventStartingOnSummerAndNowAboutHourChangingShouldWork() {
        final OffsetDateTime startDateTimeOnParis = dateTimeOnParis(2017, 7, 29, 3, 0);
        final OffsetDateTime endDateTimeOnParis = dateTimeOnParis(2017, 7, 29, 4, 30);
        assertThat(startDateTimeOnParis.withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 7, 29, 1, 0)));
        assertThat(endDateTimeOnParis.withOffsetSameInstant(ZoneOffset.UTC), is(dateTimeInUTC(2017, 7, 29, 2, 30)));
        assertThat(dateTimeOnParis(2017, 10, 28, 23, 59).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 10, 28, 21, 59)));
        assertThat(dateTimeOnParis(2017, 10, 29, 0, 0).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 10, 28, 22, 0)));
        assertThat(dateTimeOnParis(2017, 10, 29, 2, 0).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 10, 29, 0, 0)));
        assertThat(dateTimeOnParis(2017, 10, 29, 2, 59).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 10, 29, 0, 59)));
        assertThat(dateTimeOnParis(2017, 10, 29, 3, 0).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2017, 10, 29, 2, 0)));
        assertThat(dateTimeOnParis(2018, 3, 25, 2, 59).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2018, 3, 25, 1, 59)));
        assertThat(dateTimeOnParis(2018, 3, 25, 3, 0).withOffsetSameInstant(ZoneOffset.UTC),
                is(dateTimeInUTC(2018, 3, 25, 1, 0)));
        CalendarEvent recurrentEvent = calendarEventForTest(
                Period.between(startDateTimeOnParis, endDateTimeOnParis), PARIS_ZONE_ID)
                        .recur(Recurrence.every(1, TimeUnit.MONTH).until(100));
        ZonedDateTime from = ZonedDateTime.parse("2017-10-29T00:59:59-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 10, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 10, 29, 3, 30)));

        from = ZonedDateTime.parse("2017-10-29T01:00:00-01:00[Atlantic/Azores]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 11, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 11, 29, 3, 30)));

        from = ZonedDateTime.parse("2017-10-29T01:59:59+00:00[UTC]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 10, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 10, 29, 3, 30)));

        from = ZonedDateTime.parse("2017-10-29T02:00:00+00:00[UTC]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 11, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 11, 29, 3, 30)));

        from = ZonedDateTime.parse("2017-10-29T02:59:59+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 10, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 10, 29, 3, 30)));

        from = ZonedDateTime.parse("2017-10-29T03:00:00+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 11, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 11, 29, 3, 30)));

        from = ZonedDateTime.parse("2018-01-29T02:59:59+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 1, 29, 2, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 1, 29, 3, 30)));

        from = ZonedDateTime.parse("2018-01-29T03:00:00+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 3, 29, 1, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 3, 29, 2, 30)));

        from = ZonedDateTime.parse("2018-02-28T02:59:59+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 3, 29, 1, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 3, 29, 2, 30)));

        from = ZonedDateTime.parse("2018-02-28T03:00:00+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 3, 29, 1, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 3, 29, 2, 30)));

        from = ZonedDateTime.parse("2018-03-29T02:59:59+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 3, 29, 1, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 3, 29, 2, 30)));

        from = ZonedDateTime.parse("2018-03-29T03:00:00+01:00[Europe/Paris]");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2018, 4, 29, 1, 0)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2018, 4, 29, 2, 30)));
    }

    @Test
    public void nextOccurrenceAboutRecurrentHourEventWithExceptionShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(
                Period.between(dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 12, 14, 45)))
                        .recur(Recurrence.every(1, TimeUnit.DAY).until(3).excludeEventOccurrencesStartingAt(
                                dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 14, 13, 30)));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 13, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:29:59Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 13, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 13, 14, 45)));

        from = ZonedDateTime.parse("2017-12-13T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentHugeHourEventShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(
                Period.between(dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 15, 14, 45)))
                        .recur(Recurrence.every(1, TimeUnit.DAY).until(3));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 15, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:29:59Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 12, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 15, 14, 45)));

        from = ZonedDateTime.parse("2017-12-12T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 16, 14, 45)));

        from = ZonedDateTime.parse("2017-12-13T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 14, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 17, 14, 45)));

        from = ZonedDateTime.parse("2017-12-14T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    @Test
    public void nextOccurrenceAboutRecurrentHugeHourEventWithExceptionShouldWork() {
        CalendarEvent recurrentEvent = calendarEventForTest(
                Period.between(dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 15, 14, 45)))
                        .recur(Recurrence.every(1, TimeUnit.DAY).until(3).excludeEventOccurrencesStartingAt(
                                dateTimeInUTC(2017, 12, 12, 13, 30), dateTimeInUTC(2017, 12, 14, 13, 30)));
        ZonedDateTime from = ZonedDateTime.parse("2000-01-01T11:11:11-01:00[Atlantic/Azores]");
        CalendarEventOccurrence result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 16, 14, 45)));

        from = ZonedDateTime.parse("2017-12-13T13:29:59Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, notNullValue());
        assertThat(result.getStartDate(), is(dateTimeInUTC(2017, 12, 13, 13, 30)));
        assertThat(result.getEndDate(), is(dateTimeInUTC(2017, 12, 16, 14, 45)));

        from = ZonedDateTime.parse("2017-12-13T13:30:00Z");
        result = generator.generateNextOccurrenceOf(recurrentEvent, from);
        assertThat(result, nullValue());
    }

    private static List<CalendarEvent> calendarEventsForTest() {
        List<CalendarEvent> events = new ArrayList<>();
        /* event 1 on Thursday 2016-08-11 */
        events.add(CalendarEvent.on(date(2016, 8, 11)).withTitle(EVENT_TITLE + " 1")
                .withDescription(EVENT_DESCRIPTION + " 1").withAttribute(ATTR_TEST_ID, "1"));
        /* event 2 at Friday 2016-05-20 15h00 - 15h35 */
        events.add(CalendarEvent
                .on(Period.between(dateTimeInUTC(2016, 5, 20, 15, 0), dateTimeInUTC(2016, 5, 20, 15, 35)))
                .withTitle(EVENT_TITLE + " 2").withDescription(EVENT_DESCRIPTION + " 2")
                .withAttribute(ATTR_TEST_ID, "2"));
        /* event 3 at 09h00 - 09h15 every Fridays from 2016-03-04 excluding
           Friday 2016-07-15 and Friday 2016-07-22 */
        events.add(
                CalendarEvent.on(Period.between(dateTimeInUTC(2016, 3, 4, 9, 0), dateTimeInUTC(2016, 3, 4, 9, 15)))
                        .withTitle(EVENT_TITLE + " 3").withDescription(EVENT_DESCRIPTION + " 3")
                        .withAttribute(ATTR_TEST_ID, "3").recur(Recurrence.every(WEEK).on(FRIDAY)
                                .excludeEventOccurrencesStartingAt(date(2016, 7, 15), date(2016, 7, 22))));
        /* event 4 from Monday 2016-07-11 to Friday 2016-07-22 */
        events.add(
                CalendarEvent.on(Period.between(date(2016, 7, 11), date(2016, 7, 22))).withTitle(EVENT_TITLE + " 4")
                        .withDescription(EVENT_DESCRIPTION + " 4").withAttribute(ATTR_TEST_ID, "4"));
        /* event 5 at 10h00 - 11h00 every Monday, Tuesday and Wednesday from Thursday 2016-09-01 to
           Tuesday 2016-12-20 excluding Wednesday 2016-11-30 and Monday 2016-12-12 */
        events.add(
                CalendarEvent.on(Period.between(dateTimeInUTC(2016, 9, 1, 10, 0), dateTimeInUTC(2016, 9, 1, 11, 0)))
                        .withTitle(EVENT_TITLE + " 5").withDescription(EVENT_DESCRIPTION + " 5")
                        .withAttribute(ATTR_TEST_ID, "5")
                        .recur(Recurrence.every(WEEK).on(MONDAY, TUESDAY, WEDNESDAY)
                                .until(dateTimeInUTC(2016, 12, 20, 10, 0))
                                .excludeEventOccurrencesStartingAt(date(2016, 11, 30), date(2016, 12, 12))));
        /* event 6 at 08h00 - 09h00 every month on all Thursdays and on the third Friday
           from Thursday 2016-04-28 to Friday 2016-07-01 */
        events.add(
                CalendarEvent.on(Period.between(dateTimeInUTC(2016, 4, 28, 8, 0), dateTimeInUTC(2016, 4, 28, 9, 0)))
                        .withTitle(EVENT_TITLE + " 6").withDescription(EVENT_DESCRIPTION + " 6")
                        .withAttribute(ATTR_TEST_ID, "6")
                        .recur(Recurrence.every(MONTH)
                                .on(DayOfWeekOccurrence.all(THURSDAY), DayOfWeekOccurrence.nth(3, FRIDAY))
                                .until(date(2016, 6, 30))));

        Calendar calendar = new Calendar();
        calendar.setZoneId(UTC_ZONE_ID);
        for (CalendarEvent event : events) {
            try {
                FieldUtils.writeDeclaredField(event.asCalendarComponent(), "calendar", calendar, true);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        return events;
    }

    private CalendarEvent calendarEventForTest(Period period) {
        return calendarEventForTest(period, UTC_ZONE_ID);
    }

    private CalendarEvent calendarEventForTest(Period period, ZoneId calendarZoneId) {
        CalendarEvent event = CalendarEvent.on(period).withTitle(EVENT_TITLE).withDescription(EVENT_DESCRIPTION);
        Calendar calendar = new Calendar();
        calendar.setZoneId(calendarZoneId);
        try {
            FieldUtils.writeDeclaredField(event.asCalendarComponent(), "calendar", calendar, true);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        return event;
    }

    private Period in(Year year) {
        return Period.between(year.atDay(1).atStartOfDay().atOffset(ZoneOffset.UTC), year.atMonth(DECEMBER)
                .atEndOfMonth().plusDays(1).atStartOfDay().minusMinutes(1).atOffset(ZoneOffset.UTC));
    }

    private Period in(YearMonth yearMonth) {
        return Period.between(yearMonth.atDay(1).atStartOfDay().atOffset(ZoneOffset.UTC),
                yearMonth.atEndOfMonth().plusDays(1).atStartOfDay().minusMinutes(1).atOffset(ZoneOffset.UTC));
    }

    private static LocalDate date(int year, int month, int day) {
        return LocalDate.of(year, month, day);
    }

    private static OffsetDateTime dateTimeInUTC(int year, int month, int day, int hour, int minute) {
        return OffsetDateTime.of(year, month, day, hour, minute, 0, 0, ZoneOffset.UTC);
    }

    private static OffsetDateTime dateTimeOnParis(int year, int month, int day, int hour, int minute) {
        return ZonedDateTime.of(year, month, day, hour, minute, 0, 0, PARIS_ZONE_ID).toOffsetDateTime();
    }

    static {
        // This static block permits to ensure that the UNIT TEST is entirely executed into UTC
        // TimeZone.
        TimeZone.setDefault(TimeZone.getTimeZone(UTC_ZONE_ID));
    }
}