org.silverpeas.core.webapi.calendar.CalendarWebManager.java Source code

Java tutorial

Introduction

Here is the source code for org.silverpeas.core.webapi.calendar.CalendarWebManager.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:
 * "http://www.silverpeas.org/docs/core/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.webapi.calendar;

import org.apache.commons.lang3.tuple.Pair;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.admin.component.model.PersonalComponent;
import org.silverpeas.core.admin.component.model.PersonalComponentInstance;
import org.silverpeas.core.admin.component.model.SilverpeasPersonalComponentInstance;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.annotation.Base;
import org.silverpeas.core.calendar.Attendee;
import org.silverpeas.core.calendar.Attendee.ParticipationStatus;
import org.silverpeas.core.calendar.Calendar;
import org.silverpeas.core.calendar.CalendarEvent;
import org.silverpeas.core.calendar.CalendarEvent.EventOperationResult;
import org.silverpeas.core.calendar.CalendarEventOccurrence;
import org.silverpeas.core.calendar.CalendarEventOccurrenceGenerator;
import org.silverpeas.core.calendar.ICalendarEventImportProcessor;
import org.silverpeas.core.calendar.ICalendarImportResult;
import org.silverpeas.core.calendar.Plannable;
import org.silverpeas.core.calendar.icalendar.ICalendarExporter;
import org.silverpeas.core.calendar.view.CalendarEventInternalParticipationView;
import org.silverpeas.core.contribution.attachment.model.SimpleDocumentPK;
import org.silverpeas.core.date.Period;
import org.silverpeas.core.importexport.ExportDescriptor;
import org.silverpeas.core.importexport.ExportException;
import org.silverpeas.core.importexport.ImportException;
import org.silverpeas.core.persistence.datasource.model.IdentifiableEntity;
import org.silverpeas.core.util.LocalizationBundle;
import org.silverpeas.core.util.Mutable;
import org.silverpeas.core.util.ResourceLocator;
import org.silverpeas.core.util.ServiceProvider;
import org.silverpeas.core.util.SettingBundle;
import org.silverpeas.core.util.logging.SilverLogger;
import org.silverpeas.core.web.mvc.webcomponent.WebMessager;

import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.silverpeas.core.calendar.CalendarEventOccurrence.COMPARATOR_BY_DATE_ASC;
import static org.silverpeas.core.calendar.CalendarEventUtil.getDateWithOffset;
import static org.silverpeas.core.contribution.attachment.AttachmentServiceProvider.getAttachmentService;
import static org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController.wysiwygPlaceHaveChanged;
import static org.silverpeas.core.util.StringUtil.defaultStringIfNotDefined;
import static org.silverpeas.core.util.StringUtil.isNotDefined;
import static org.silverpeas.core.webapi.calendar.OccurrenceEventActionMethodType.ALL;
import static org.silverpeas.core.webapi.calendar.OccurrenceEventActionMethodType.UNIQUE;

/**
 * @author Yohann Chastagnier
 */
@Singleton
@Base
@Named("default" + CalendarWebManager.NAME_SUFFIX)
public class CalendarWebManager {

    /**
     * The predefined suffix that must compound the name of each implementation of this interface.
     * An implementation of this interface by a Silverpeas application named Kmelia must be named
     * <code>kmelia[NAME_SUFFIX]</code> where NAME_SUFFIX is the predefined suffix as defined below.
     */
    public static final String NAME_SUFFIX = "CalendarWebManager";

    private static final SettingBundle settings = ResourceLocator
            .getSettingBundle("org.silverpeas.calendar.settings.calendar");
    private final SilverLogger silverLogger = SilverLogger.getLogger(Plannable.class);

    private static final int END_YEAR_OFFSET = 3;
    private static final int DEFAULT_NB_MAX_NEXT_OCC = 10;

    @Inject
    private ICalendarExporter iCalendarExporter;

    @Inject
    private CalendarEventOccurrenceGenerator generator;

    @Inject
    private ICalendarEventImportProcessor iCalendarEventImportProcessor;

    protected CalendarWebManager() {
    }

    /**
     * Gets the singleton instance of the provider.
     * @param componentInstanceIdOrComponentName a component instance identifier of a component name.
     * @see ServiceProvider#getServiceByComponentInstanceAndNameSuffix(String, String)
     */
    public static CalendarWebManager get(final String componentInstanceIdOrComponentName) {
        if (isNotDefined(componentInstanceIdOrComponentName)) {
            return ServiceProvider.getService(CalendarWebManager.class, new AnnotationLiteral<Base>() {
            });
        }
        return ServiceProvider.getServiceByComponentInstanceAndNameSuffix(componentInstanceIdOrComponentName,
                NAME_SUFFIX);
    }

    /**
     * Asserts the consistency of given data, otherwise an HTTP error is sent back.<br>
     * Calendar must exists and be linked to the component instance represented bu given identifier.
     * @param componentInstanceId the identifier of current handled component instance.
     * @param originalCalendar the calendar to check against the other data.
     */
    static void assertDataConsistency(final String componentInstanceId, final Calendar originalCalendar) {
        assertEntityIsDefined(originalCalendar);
        if (!originalCalendar.getComponentInstanceId().equals(componentInstanceId)) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
    }

    /**
     * Asserts the consistency of given data, otherwise an HTTP error is sent back.<br>
     * Checks of {@link #assertDataConsistency(String, Calendar)} is performed.
     * And also the followings ones:
     * <ul>
     * <li>all entities must be linked to the given component instance identifier</li>
     * <li>the calendar of previous event data must be equal</li>
     * <li>the identifier must be equal between old and new data</li>
     * </ul>
     * @param componentInstanceId the identifier of current handled component instance.
     * @param originalCalendar the calendar to check against the other data.
     * @param event the event to check against the others.
     */
    static void assertDataConsistency(final String componentInstanceId, final Calendar originalCalendar,
            final CalendarEvent event) {
        assertDataConsistency(componentInstanceId, originalCalendar);
        assertEntityIsDefined(event.asCalendarComponent());
        // Checking the component instance id.
        if (!originalCalendar.getComponentInstanceId().equals(componentInstanceId)
                || !event.getCalendar().getComponentInstanceId().equals(componentInstanceId)) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        // Checking event data which must be linked to the original calendar
        if (!event.getCalendar().getId().equals(originalCalendar.getId())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
    }

    /**
     * Asserts the consistency of given data, otherwise an HTTP error is sent back.<br>
     * Checks of {@link #assertDataConsistency(String, Calendar, CalendarEvent)} is performed.
     * And also the followings ones:
     * <ul>
     * <li>all entities must be linked to the given component instance identifier</li>
     * <li>the calendar of previous event data must be equal</li>
     * <li>the identifier must be equal between old and new data</li>
     * </ul>
     * @param componentInstanceId the identifier of current handled component instance.
     * @param originalCalendar the calendar to check against the other data.
     * @param previousOne the previous event data to check against the others.
     * @param occurrence the occurrence data to check against the others.
     */
    static void assertDataConsistency(final String componentInstanceId, final Calendar originalCalendar,
            final CalendarEvent previousOne, final CalendarEventOccurrence occurrence) {
        assertDataConsistency(componentInstanceId, originalCalendar, previousOne);
        assertEntityIsDefined(occurrence);
        // Checking the component instance id with the new event data.
        if (!occurrence.getCalendarEvent().getCalendar().getComponentInstanceId().equals(componentInstanceId)) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        // Checking previous and old event data
        if (!previousOne.getId().equals(occurrence.getCalendarEvent().getId())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
    }

    /**
     * Asserts the specified entity is well defined, otherwise an HTTP 404 error is sent back.
     * @param entity the entity to check.
     */
    static void assertEntityIsDefined(final IdentifiableEntity entity) {
        if (entity == null || isNotDefined(entity.getId())) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }

    /**
     * Creates an event from the given calendar and event data.<br>
     * This method handles also a common behavior the UI must have between each way an event is
     * saved (from a controller, a WEB service...)
     * @param calendar the calendar on which the event is added.
     * @param event the event to create.
     * @param volatileEventId the volatile identifier used to attach the images on WYSIWYG editor.
     * @return the calendar event.
     */
    public CalendarEvent createEvent(Calendar calendar, CalendarEvent event, String volatileEventId) {
        if (!calendar.canBeAccessedBy(User.getCurrentRequester())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        event.planOn(calendar);

        // Attaching all documents linked to volatile identifier to the persisted one
        final String finalEventId = event.getId();
        final String instanceId = calendar.getComponentInstanceId();
        final ResourceReference volatileAttachmentSourcePK = new ResourceReference(volatileEventId, instanceId);
        final ResourceReference finalAttachmentSourcePK = new ResourceReference(finalEventId, instanceId);
        final List<SimpleDocumentPK> movedDocumentPks = getAttachmentService()
                .moveAllDocuments(volatileAttachmentSourcePK, finalAttachmentSourcePK);
        if (!movedDocumentPks.isEmpty()) {
            // Change images path in wysiwyg
            wysiwygPlaceHaveChanged(instanceId, volatileEventId, instanceId, finalEventId);
        }

        successMessage("calendar.message.event.created", event.getTitle());
        return event;
    }

    /**
     * Gets the common calendar bundle according to the given locale.
     * @param locale the locale into which the requested bundle must be set.
     * @return a localized bundle.
     */
    public LocalizationBundle getLocalizationBundle(String locale) {
        return ResourceLocator.getLocalizationBundle("org.silverpeas.calendar.multilang.calendarBundle", locale);
    }

    /**
     * Gets all calendars handled by a component instance.
     * <p>This centralization is useful for components which handles other agendas than those linked
     * to the instance.</p>
     * @param componentInstanceId the identifier of the component instance.
     * @return the list of calendars.
     */
    public List<Calendar> getCalendarsHandledBy(final String componentInstanceId) {
        return Calendar.getByComponentInstanceId(componentInstanceId);
    }

    /**
     * Saves the given calendar.<br>
     * This method handles also a common behavior the UI must have between each way a calendar is
     * saved (from a controller, a WEB service...)
     * @param calendar the calendar to save.
     * @return the calendar.
     */
    Calendar saveCalendar(Calendar calendar) {
        User owner = User.getCurrentRequester();
        String successfulMessageKey = calendar.isPersisted() ? "calendar.message.calendar.updated"
                : "calendar.message.calendar.created";
        if (calendar.isPersisted() && !calendar.canBeModifiedBy(User.getCurrentRequester())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        calendar.save();
        String userLanguage = owner.getUserPreferences().getLanguage();
        getMessager().addSuccess(
                getLocalizationBundle(userLanguage).getStringWithParams(successfulMessageKey, calendar.getTitle()));
        return calendar;
    }

    /**
     * Deletes the given calendar.<br>
     * This method handles also a common behavior the UI must have between each way a calendar is
     * deleted (from a controller, a WEB service...)
     * @param calendar the calendar to delete.
     */
    void deleteCalendar(Calendar calendar) {
        User owner = User.getCurrentRequester();
        if (!calendar.canBeDeletedBy(User.getCurrentRequester())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        calendar.delete();
        String userLanguage = owner.getUserPreferences().getLanguage();
        getMessager().addSuccess(getLocalizationBundle(userLanguage)
                .getStringWithParams("calendar.message.calendar.deleted", calendar.getTitle()));
    }

    /**
     * Exports the given calendar into ICalendar format.
     * @param calendar the calendar to export.
     * @param descriptor the export descriptor.
     * @throws ExportException on export exception.
     */
    void exportCalendarAsICalendarFormat(final Calendar calendar, final ExportDescriptor descriptor)
            throws ExportException {
        final Mutable<User> currentUser = Mutable.ofNullable(User.getCurrentRequester());
        if (!currentUser.isPresent()) {
            SilverpeasPersonalComponentInstance.getById(calendar.getComponentInstanceId())
                    .ifPresent(i -> currentUser.set(i.getUser()));
        }
        if (currentUser.isPresent() && calendar.isMainPersonalOf(currentUser.get())) {
            iCalendarExporter.exports(descriptor,
                    () -> Calendar.getEvents().filter(f -> f.onParticipants(currentUser.get())).stream());
        } else {
            iCalendarExporter.exports(descriptor,
                    () -> Calendar.getEvents().filter(f -> f.onCalendar(calendar)).stream());
        }
    }

    /**
     * Synchronizes the given calendar.
     * <p>Throws a forbidden WEB application exception if the calendar is not a synchronized one</p>
     * @param calendar the calendar to synchronize.
     */
    void synchronizeCalendar(final Calendar calendar) throws ImportException {
        if (calendar.getExternalCalendarUrl() == null) {
            throw new WebApplicationException("aimed calendar is not a synchronized one",
                    Response.Status.FORBIDDEN);
        }
        final String calendarTitle = calendar.getTitle();
        final String calendarId = calendar.getId();
        silverLogger.info("start event synchronization of calendar {0} (id={1})", calendarTitle, calendarId);
        ICalendarImportResult result = calendar.synchronize();

        silverLogger.info(
                "end event synchronization of calendar {0} (id={1}), with {2} created events, {3} updated"
                        + " events and {4} deleted events",
                calendarTitle, calendarId, result.added(), result.updated(), result.deleted());
        successMessage("calendar.message.calendar.synchronized", calendar.getTitle(), result.added(),
                result.updated(), result.deleted());
    }

    /**
     * Imports the calendar events into the specified calendar from the specified input stream.
     * @param inputStream an input stream from which the serialized calendar events can be imported.
     */
    void importEventsAsICalendarFormat(final Calendar calendar, final InputStream inputStream)
            throws ImportException {
        final String calendarTitle = calendar.getTitle();
        final String calendarId = calendar.getId();
        silverLogger.info("start event import into calendar {0} (id={1})", calendarTitle, calendarId);

        ICalendarImportResult result = iCalendarEventImportProcessor.importInto(calendar, inputStream);

        silverLogger.info(
                "end event import into calendar {0} (id={1}), with {2} created events and {3} updated " + "events",
                calendarTitle, calendarId, result.added(), result.updated());
        successMessage("calendar.message.event.imported", calendarTitle, result.added(), result.updated());
    }

    /**
     * Saves an event occurrence.<br>
     * This method handles also a common behavior the UI must have between each way an event is
     * saved (from a controller, a WEB service...)
     * @param occurrence the occurrence to save.
     * @param updateMethodType indicates the method of the occurrence update.
     * @param zoneId the zoneId into which dates are displayed (optional).  @return the calendar
     * event.
     */
    List<CalendarEvent> saveOccurrence(final CalendarEventOccurrence occurrence,
            OccurrenceEventActionMethodType updateMethodType, final ZoneId zoneId) {
        if (!occurrence.getCalendarEvent().canBeModifiedBy(User.getCurrentRequester())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        OccurrenceEventActionMethodType methodType = updateMethodType == null ? ALL : updateMethodType;

        final String originalTitle = occurrence.getCalendarEvent().getTitle();
        final Temporal originalStartDate = occurrence.getOriginalStartDate();

        final EventOperationResult result;
        switch (methodType) {
        case FROM:
            result = occurrence.updateSinceMe();
            break;
        case UNIQUE:
            result = occurrence.update();
            break;
        default:
            final CalendarEvent event = occurrence.getCalendarEvent();
            occurrence.asCalendarComponent().copyTo(event.asCalendarComponent());
            result = event.update();
            break;
        }

        final List<CalendarEvent> events = new ArrayList<>();
        Optional<CalendarEvent> createdEvent = result.created();
        Optional<CalendarEvent> updatedEvent = result.updated();
        Optional<CalendarEventOccurrence> updatedOccurrence = result.instance();

        updatedOccurrence.ifPresent(o -> {
            final CalendarEvent event = o.getCalendarEvent();
            successMessage("calendar.message.event.occurrence.updated.unique", originalTitle, getMessager()
                    .formatDate(getDateWithOffset(event.asCalendarComponent(), originalStartDate, zoneId)));
            events.add(event);
        });

        updatedEvent.ifPresent(e -> {
            if (!createdEvent.isPresent()) {
                successMessage("calendar.message.event.updated", e.getTitle());
            } else {
                //noinspection OptionalGetWithoutIsPresent
                final Temporal endDate = e.getRecurrence().getRecurrenceEndDate().get();
                successMessage("calendar.message.event.occurrence.updated.from", e.getTitle(),
                        getMessager().formatDate(getDateWithOffset(e.asCalendarComponent(), endDate, zoneId)));
            }
            events.add(e);
        });

        createdEvent.ifPresent(e -> {
            events.add(e);
            successMessage("calendar.message.event.created", e.getTitle());
        });

        return events;
    }

    /**
     * Deletes occurrences of an event from the given occurrence.<br>
     * This method handles also a common behavior the UI must have between each way an event is
     * deleted (from a controller, a WEB service...)
     * @param occurrence the occurrence to delete.
     * @param deleteMethodType indicates the method of the occurrence deletion.
     * @param zoneId the zoneId into which dates are displayed (optional).
     */
    CalendarEvent deleteOccurrence(CalendarEventOccurrence occurrence,
            OccurrenceEventActionMethodType deleteMethodType, final ZoneId zoneId) {
        if (!occurrence.getCalendarEvent().canBeDeletedBy(User.getCurrentRequester())) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        OccurrenceEventActionMethodType methodType = deleteMethodType == null ? ALL : deleteMethodType;

        final EventOperationResult result;
        switch (methodType) {
        case FROM:
            result = occurrence.deleteSinceMe();
            break;
        case UNIQUE:
            result = occurrence.delete();
            break;
        default:
            result = occurrence.getCalendarEvent().delete();
            break;
        }

        Optional<CalendarEvent> updatedEvent = result.updated();
        if (!updatedEvent.isPresent() || !updatedEvent.get().isRecurrent()) {
            successMessage("calendar.message.event.deleted", occurrence.getTitle());
        } else {
            final String bundleKey;
            final Temporal endDate;
            if (methodType == UNIQUE) {
                bundleKey = "calendar.message.event.occurrence.deleted.unique";
                endDate = occurrence.getOriginalStartDate();
            } else {
                bundleKey = "calendar.message.event.occurrence.deleted.from";
                //noinspection OptionalGetWithoutIsPresent
                endDate = updatedEvent.get().getRecurrence().getRecurrenceEndDate().get();
            }
            successMessage(bundleKey, occurrence.getTitle(),
                    getMessager().formatDate(getDateWithOffset(occurrence.asCalendarComponent(), endDate, zoneId)));
        }

        return updatedEvent.orElse(null);
    }

    /**
     * Updates the participation of an attendee of an event or on an occurrence of an event from
     * the given data.<br>
     * This method handles also a common behavior the UI must have between each way an event is
     * deleted (from a controller, a WEB service...)
     * @param occurrence the occurrence.
     * @param attendeeId the identifier of the attendee which answered.
     * @param participationStatus the participation answer of the attendee.
     * @param answerMethodType indicates the method of the occurrence deletion.
     * @param zoneId the zoneId into which dates are displayed (optional).
     */
    CalendarEvent updateOccurrenceAttendeeParticipation(CalendarEventOccurrence occurrence, String attendeeId,
            ParticipationStatus participationStatus, OccurrenceEventActionMethodType answerMethodType,
            final ZoneId zoneId) {
        OccurrenceEventActionMethodType methodType = answerMethodType == null ? ALL : answerMethodType;
        CalendarEvent modifiedEvent = null;
        if (methodType == UNIQUE) {
            Optional<EventOperationResult> optionalResult = updateSingleOccurrenceAttendeeParticipation(occurrence,
                    attendeeId, participationStatus);
            if (optionalResult.isPresent()) {
                modifiedEvent = optionalResult.get().instance().get().getCalendarEvent();
                successMessage("calendar.message.event.occurrence.attendee.participation.updated.unique",
                        occurrence.getTitle(),
                        getMessager().formatDate(getDateWithOffset(occurrence.asCalendarComponent(),
                                occurrence.getOriginalStartDate(), zoneId)));
            }
        } else if (methodType == ALL) {
            Optional<EventOperationResult> optionalResult = updateEventAttendeeParticipation(occurrence, attendeeId,
                    participationStatus);
            if (optionalResult.isPresent()) {
                if (optionalResult.get().updated().isPresent()) {
                    modifiedEvent = optionalResult.get().updated().get();
                } else {
                    modifiedEvent = optionalResult.get().instance().get().getCalendarEvent();
                }
                successMessage("calendar.message.event.attendee.participation.updated",
                        occurrence.getCalendarEvent().getTitle());
            }
        }
        if (modifiedEvent == null) {
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
        return modifiedEvent;
    }

    private Optional<EventOperationResult> updateEventAttendeeParticipation(
            final CalendarEventOccurrence occurrence, final String attendeeId,
            final ParticipationStatus participationStatus) {
        EventOperationResult result = null;
        final Optional<Attendee> attendee = occurrence.getCalendarEvent().getAttendees().stream()
                .filter(a -> a.getId().equals(attendeeId)).findFirst();
        if (attendee.isPresent()) {
            setAttendeeStatus(participationStatus, attendee.get());
            result = occurrence.getCalendarEvent().update();
        } else {
            // It is the particular case where the attendee is set on occurrences but not on the
            // original event.
            List<CalendarEventOccurrence> allOccurrences = occurrence.getCalendarEvent().getPersistedOccurrences();
            for (CalendarEventOccurrence eventOccurrence : allOccurrences) {
                Optional<EventOperationResult> optionalResult = updateSingleOccurrenceAttendeeParticipation(
                        eventOccurrence, attendeeId, participationStatus);
                if (result == null && optionalResult.isPresent()) {
                    result = optionalResult.get();
                }
            }
        }
        return Optional.ofNullable(result);
    }

    private Optional<EventOperationResult> updateSingleOccurrenceAttendeeParticipation(
            final CalendarEventOccurrence occurrence, final String attendeeId,
            final ParticipationStatus participationStatus) {
        final Optional<Attendee> attendee = occurrence.getAttendees().stream()
                .filter(a -> a.getId().equals(attendeeId)).findFirst();
        if (attendee.isPresent()) {
            setAttendeeStatus(participationStatus, attendee.get());
            return Optional.of(occurrence.update());
        }
        return Optional.empty();
    }

    private void setAttendeeStatus(final ParticipationStatus participationStatus, final Attendee attendee) {
        switch (participationStatus) {
        case ACCEPTED:
            attendee.accept();
            break;
        case DECLINED:
            attendee.decline();
            break;
        case TENTATIVE:
            attendee.tentativelyAccept();
            break;
        default:
            throw new WebApplicationException(Response.Status.FORBIDDEN);
        }
    }

    /**
     * Gets the first occurrence of an event from the identifier of an event.
     * @param eventId an event identifier.
     * @return the first {@link CalendarEventOccurrence} instance of an event.
     */
    public CalendarEventOccurrence getFirstCalendarEventOccurrenceFromEventId(final String eventId) {
        CalendarEvent event = CalendarEvent.getById(eventId);
        final Temporal startTemporal = event.getStartDate();
        final Temporal endTemporal;
        if (!event.isRecurrent()) {
            endTemporal = event.getEndDate();
        } else {
            endTemporal = event.getEndDate().plus(END_YEAR_OFFSET, ChronoUnit.YEARS);
        }
        return generator.generateOccurrencesOf(singletonList(event), Period.between(startTemporal, endTemporal))
                .get(0);
    }

    /**
     * Gets the event occurrences associated to a calendar and contained a the time window specified
     * by the start and end datetimes.<br>
     * The occurrences are sorted from the lowest to the highest date.
     * @param startDate the start date of time window.
     * @param endDate the end date of time window.
     * @param calendars the calendars the event occurrences belong to.
     * @return a list of entities of calendar event occurrences.
     */
    public List<CalendarEventOccurrence> getEventOccurrencesOf(LocalDate startDate, LocalDate endDate,
            List<Calendar> calendars) {
        return calendars.isEmpty() ? emptyList()
                : Calendar.getTimeWindowBetween(startDate, endDate).filter(f -> f.onCalendar(calendars))
                        .getEventOccurrences();
    }

    /**
     * Gets all event occurrences associated to users and contained a the time window specified
     * by the start and end date times.<br>
     * Attendees which have answered negatively about their presence are not taken into account.
     * The occurrences are sorted from the lowest to the highest date and mapped by user identifiers.
     * @param currentUserAndComponentInstanceId the current user and the current component instance
     * ids from which the service is requested.
     * @param startDate the start date of time window.
     * @param endDate the end date of time window.
     * @param users the users to filter on.
     * @return a list of entities of calendar event occurrences mapped by user identifiers.
     */
    protected Map<String, List<CalendarEventOccurrence>> getAllEventOccurrencesByUserIds(
            final Pair<List<String>, User> currentUserAndComponentInstanceId, LocalDate startDate,
            LocalDate endDate, Collection<User> users) {
        // Retrieving the occurrences from personal calendars
        final List<Calendar> personalCalendars = new ArrayList<>();
        users.forEach(u -> personalCalendars.addAll(getCalendarsHandledBy(
                PersonalComponentInstance.from(u, PersonalComponent.getByName("userCalendar").get()).getId())));
        final List<CalendarEventOccurrence> entities = personalCalendars.isEmpty() ? emptyList()
                : Calendar.getTimeWindowBetween(startDate, endDate).filter(f -> f.onCalendar(personalCalendars))
                        .getEventOccurrences();
        entities.addAll(Calendar.getTimeWindowBetween(startDate, endDate).filter(f -> f.onParticipants(users))
                .getEventOccurrences());
        // Getting the occurrences by users
        Map<String, List<CalendarEventOccurrence>> result = new CalendarEventInternalParticipationView(users)
                .apply(entities.stream().distinct().collect(Collectors.toList()));
        final String currentUserId = currentUserAndComponentInstanceId.getRight().getId();
        if (result.containsKey(currentUserId)) {
            List<CalendarEventOccurrence> currentUserOccurrences = result.get(currentUserId);
            // Remove occurrence associated to given user when he is the creator
            currentUserOccurrences.removeIf(calendarEventOccurrence -> {
                CalendarEvent event = calendarEventOccurrence.getCalendarEvent();
                return currentUserAndComponentInstanceId.getLeft()
                        .contains(event.getCalendar().getComponentInstanceId())
                        && event.getCreator().getId().equals(currentUserId);
            });
        } else {
            result.put(currentUserId, emptyList());
        }
        return result;
    }

    /**
     * Gets the next event occurrences from now.
     * @param componentIds identifiers of aimed component instance.
     * @param calendarIdsToExclude identifier of calendars which linked occurrences must be excluded
     * from the result.
     * @param usersToInclude identifiers of users which linked occurrences must be included into the
     * result
     * @param calendarIdsToInclude identifier of calendars which linked occurrences must be included
     * into the result.
     * @param zoneId the identifier of the zone.
     * @param limit the maximum occurrences the result must have (must be lower than 500)
     * @return a list of {@link CalendarEventOccurrence}.
     */
    public Stream<CalendarEventOccurrence> getNextEventOccurrences(final List<String> componentIds,
            final Set<String> calendarIdsToExclude, final Set<User> usersToInclude,
            final Set<String> calendarIdsToInclude, final ZoneId zoneId, final Integer limit) {
        final User currentRequester = User.getCurrentRequester();
        // load calendars
        final List<Calendar> calendars = componentIds.stream().flatMap(i -> getCalendarsHandledBy(i).stream())
                .distinct().collect(Collectors.toList());
        // includes/excludes
        calendarIdsToInclude.removeAll(calendarIdsToExclude);
        calendars.removeIf(c -> calendarIdsToExclude.contains(c.getId()));
        if (!calendarIdsToInclude.isEmpty()) {
            calendars.forEach(c -> calendarIdsToInclude.remove(c.getId()));
            calendarIdsToInclude.forEach(i -> {
                Calendar calendarToInclude = Calendar.getById(i);
                if (calendarToInclude.canBeAccessedBy(currentRequester)) {
                    calendars.add(calendarToInclude);
                }
            });
        }
        // loading occurrences
        final int nbOccLimit = (limit != null && limit > 0 && limit <= 500) ? limit : DEFAULT_NB_MAX_NEXT_OCC;
        final LocalDate startDate = zoneId != null ? LocalDateTime.now(zoneId).toLocalDate() : LocalDate.now();
        final Set<CalendarEventOccurrence> occurrences = new HashSet<>();
        for (int nbMonthsToAdd : getNextEventTimeWindows()) {
            occurrences.clear();
            LocalDate endDate = startDate.plusMonths(nbMonthsToAdd);
            occurrences.addAll(getEventOccurrencesOf(startDate, endDate, calendars));
            if (!usersToInclude.isEmpty()) {
                getAllEventOccurrencesByUserIds(Pair.of(componentIds, currentRequester), startDate, endDate,
                        usersToInclude).forEach((u, o) -> occurrences.addAll(o));
            }
            if (occurrences.size() >= nbOccLimit) {
                break;
            }
        }
        return occurrences.stream().sorted(COMPARATOR_BY_DATE_ASC).limit(nbOccLimit);
    }

    /**
     * Gets next event time windows from settings.
     * @return list of integer which represents months.
     */
    protected Integer[] getNextEventTimeWindows() {
        final String[] timeWindows = settings.getString("calendar.nextEvents.time.windows").split(",");
        return Arrays.stream(timeWindows).map(w -> Integer.parseInt(w.trim())).toArray(Integer[]::new);
    }

    /**
     * Push a success message to the current user.
     * @param messageKey the key of the message.
     * @param params the message parameters.
     */
    private void successMessage(String messageKey, Object... params) {
        User owner = User.getCurrentRequester();
        String userLanguage = owner.getUserPreferences().getLanguage();
        getMessager().addSuccess(getLocalizationBundle(userLanguage).getStringWithParams(messageKey, params));
    }

    private WebMessager getMessager() {
        return WebMessager.getInstance();
    }
}