org.silverpeas.core.calendar.ical4j.ICal4JImporter.java Source code

Java tutorial

Introduction

Here is the source code for org.silverpeas.core.calendar.ical4j.ICal4JImporter.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.calendar.ical4j;

import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.CalendarParserFactory;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.ParameterFactoryRegistry;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyFactoryRegistry;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.property.Categories;
import net.fortuna.ical4j.model.property.Description;
import net.fortuna.ical4j.util.CompatibilityHints;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.silverpeas.core.calendar.CalendarComponent;
import org.silverpeas.core.calendar.CalendarEvent;
import org.silverpeas.core.calendar.CalendarEventOccurrence;
import org.silverpeas.core.calendar.CalendarEventOccurrenceBuilder;
import org.silverpeas.core.calendar.Recurrence;
import org.silverpeas.core.calendar.VisibilityLevel;
import org.silverpeas.core.calendar.icalendar.ICalendarImporter;
import org.silverpeas.core.date.Period;
import org.silverpeas.core.date.TimeUnit;
import org.silverpeas.core.importexport.ImportDescriptor;
import org.silverpeas.core.importexport.ImportException;
import org.silverpeas.core.persistence.datasource.OperationContext;
import org.silverpeas.core.persistence.datasource.model.jpa.JpaEntityReflection;
import org.silverpeas.core.util.Mutable;
import org.silverpeas.core.util.ResourceLocator;
import org.silverpeas.core.util.SettingBundle;
import org.silverpeas.core.util.logging.SilverLogger;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;

import static org.apache.commons.io.IOUtils.toInputStream;
import static org.silverpeas.core.date.TimeZoneUtil.toZoneId;
import static org.silverpeas.core.util.StringUtil.isDefined;

/**
 * Implementation of the {@link ICalendarImporter} interface by using the iCal4J library to perform
 * the deserialization of calendar events in the iCalendar format.
 * @author mmoquillon
 */
public class ICal4JImporter implements ICalendarImporter {

    private static final String CALENDAR_SETTINGS = "org.silverpeas.calendar.settings.calendar";

    @Inject
    private ICal4JDateCodec iCal4JDateCodec;
    @Inject
    private ICal4JRecurrenceCodec iCal4JRecurrenceCodec;

    @PostConstruct
    private void init() {
        CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);
        CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true);
        CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_VALIDATION, true);
        CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true);
    }

    @Override
    public void imports(final ImportDescriptor descriptor,
            final Consumer<Stream<Pair<CalendarEvent, List<CalendarEventOccurrence>>>> consumer)
            throws ImportException {
        try {
            PropertyFactoryRegistry propertyFactoryRegistry = new PropertyFactoryRegistry();
            propertyFactoryRegistry.register(HtmlProperty.PROPERTY_NAME, HtmlProperty.FACTORY);
            CalendarBuilder builder = new CalendarBuilder(CalendarParserFactory.getInstance().createParser(),
                    propertyFactoryRegistry, new ParameterFactoryRegistry(),
                    TimeZoneRegistryFactory.getInstance().createRegistry());
            Calendar calendar = builder.build(getCalendarInputStream(descriptor));
            if (calendar.getComponents().isEmpty()) {
                consumer.accept(Stream.empty());
                return;
            }
            calendar.validate();

            Mutable<ZoneId> zoneId = Mutable.of(ZoneOffset.systemDefault());

            Map<String, List<VEvent>> readEvents = new LinkedHashMap<>();
            calendar.getComponents().forEach(component -> {
                if (component instanceof VEvent) {
                    VEvent vEvent = (VEvent) component;
                    String vEventId = vEvent.getUid().getValue();
                    List<VEvent> vEvents = readEvents.computeIfAbsent(vEventId, k -> new ArrayList<>());
                    if (vEvent.getRecurrenceId() != null) {
                        vEvents.add(vEvent);
                    } else {
                        vEvents.add(0, vEvent);
                    }
                } else if (component instanceof VTimeZone) {
                    VTimeZone vTimeZone = (VTimeZone) component;
                    zoneId.set(toZoneId(vTimeZone.getTimeZoneId().getValue()));
                } else {
                    SilverLogger.getLogger(this).debug("iCalendar component ''{0}'' is not handled",
                            component.getName());
                }
            });
            List<Pair<CalendarEvent, List<CalendarEventOccurrence>>> events = new ArrayList<>(readEvents.size());
            readEvents.forEach((vEventId, vEvents) -> {

                // For now the following stuffs are not handled:
                // - the attendees
                // - triggers
                VEvent vEvent = vEvents.remove(0);
                CalendarEvent event = eventFromICalEvent(zoneId, vEventId, vEvent);

                // Occurrences
                List<CalendarEventOccurrence> occurrences = new ArrayList<>(vEvents.size());
                vEvents.forEach(v -> {
                    CalendarEventOccurrence occurrence = occurrenceFromICalEvent(zoneId, event, v);
                    occurrences.add(occurrence);
                });

                if (!event.isRecurrent() && !occurrences.isEmpty()) {
                    SilverLogger.getLogger(this)
                            .warn("event with uuid {0} has no recurrence set whereas {1,choice, 1#one linked "
                                    + "occurrence exists| 1<{1} linked occurrences exist}... Setting a default "
                                    + "recurrence (RRULE:FREQ=DAILY;COUNT=1) to get correct data for Silverpeas",
                                    event.getExternalId(), occurrences.size());
                    event.recur(Recurrence.every(1, TimeUnit.DAY).until(1));
                }

                // New event to perform
                events.add(Pair.of(event, occurrences));
            });

            // The events will be performed by the caller
            consumer.accept(events.stream());

        } catch (IOException | ParserException e) {
            throw new ImportException(e);
        }
    }

    private InputStream getCalendarInputStream(final ImportDescriptor descriptor) throws IOException {
        final SettingBundle settings = ResourceLocator.getSettingBundle(CALENDAR_SETTINGS);
        final String replacements = settings.getString("calendar.import.ics.file.replace.before.process", "");
        if (isDefined(replacements)) {
            Mutable<String> icsContent = Mutable.of(IOUtils.toString(descriptor.getInputStream()));
            Arrays.stream(replacements.split(";")).map(r -> {
                String[] replacement = r.split("[/]");
                return Pair.of(replacement[0], replacement[1]);
            }).forEach(r -> {
                String previous = icsContent.get();
                icsContent.set(previous.replaceAll(r.getLeft(), r.getRight()));
            });
            return toInputStream(icsContent.get());
        }
        return descriptor.getInputStream();
    }

    private CalendarEventOccurrence occurrenceFromICalEvent(final Mutable<ZoneId> zoneId, final CalendarEvent event,
            final VEvent vEvent) {
        // The original start date
        Temporal originalStartDate = iCal4JDateCodec.decode(vEvent.getRecurrenceId().getDate(), zoneId.get());
        // The occurrence
        CalendarEventOccurrence occurrence = CalendarEventOccurrenceBuilder.forEvent(event)
                .startingAt(originalStartDate).endingAt(originalStartDate.plus(1, ChronoUnit.DAYS)).build();
        // The period
        occurrence.setPeriod(extractPeriod(zoneId, vEvent));
        // Component data
        copyICalEventToComponent(vEvent, occurrence.asCalendarComponent());
        return occurrence;
    }

    private CalendarEvent eventFromICalEvent(final Mutable<ZoneId> zoneId, final String vEventId,
            final VEvent vEvent) {
        // The period
        Period period = extractPeriod(zoneId, vEvent);
        CalendarEvent event = CalendarEvent.on(period);

        // External Id
        event.withExternalId(vEventId);

        // Visibility
        if (vEvent.getClassification() != null) {
            event.withVisibilityLevel(VisibilityLevel.valueOf(vEvent.getClassification().getValue()));
        }

        // Categories
        if (vEvent.getProperty(Categories.CATEGORIES) != null) {
            Categories categories = (Categories) vEvent.getProperty(Categories.CATEGORIES);
            Iterator<String> categoriesIt = categories.getCategories().iterator();
            while (categoriesIt.hasNext()) {
                event.getCategories().add(categoriesIt.next());
            }
        }

        // Recurrence
        if (vEvent.getProperty(Property.RRULE) != null) {
            Recurrence recurrence = iCal4JRecurrenceCodec.decode(vEvent, zoneId.get());
            event.recur(recurrence);
        }

        // Component data
        copyICalEventToComponent(vEvent, event.asCalendarComponent());
        return event;
    }

    private void copyICalEventToComponent(final VEvent vEvent, final CalendarComponent component) {
        // Title
        if (vEvent.getSummary() != null) {
            component.setTitle(vEvent.getSummary().getValue().trim());
        }

        // Description
        Property description = vEvent.getProperty(HtmlProperty.X_ALT_DESC);
        if (description == null) {
            description = vEvent.getDescription() != null ? vEvent.getDescription() : new Description("");
        }
        component.setDescription(description.getValue().trim());

        // Location
        if (vEvent.getLocation() != null) {
            component.setLocation(vEvent.getLocation().getValue().trim());
        }

        // URL
        if (vEvent.getUrl() != null) {
            component.getAttributes().set("url", vEvent.getUrl().getValue().trim());
        }

        // Priority
        if (vEvent.getPriority() != null) {
            component.setPriority(
                    org.silverpeas.core.calendar.Priority.fromICalLevel(vEvent.getPriority().getLevel()));
        }

        // Technical data which are used for detection of modifications.
        // THESE DATES MUST NOT HAVE TO BE REGISTERED INTO THE PERSISTENCE.
        // Indeed it is used by ICalendarEventImportProcessor#wasUpdated() in order to detect the
        // events modified into external calendar repository.
        if (vEvent.getCreated() != null) {
            JpaEntityReflection.setCreationData(component, OperationContext.getFromCache().getUser(),
                    vEvent.getCreated().getDate());
        }
        if (vEvent.getLastModified() != null) {
            JpaEntityReflection.setUpdateData(component, OperationContext.getFromCache().getUser(),
                    vEvent.getLastModified().getDate());
        }
    }

    private Period extractPeriod(final Mutable<ZoneId> zoneId, final VEvent vEvent) {
        final Date startDate = vEvent.getStartDate().getDate();
        Date endDate = vEvent.getEndDate().getDate();
        if (endDate == null && !(startDate instanceof DateTime)) {
            endDate = startDate;
        }
        Temporal startTemporal = iCal4JDateCodec.decode(startDate, zoneId.get());
        Temporal endTemporal = iCal4JDateCodec.decode(endDate, zoneId.get());
        if (endTemporal instanceof OffsetDateTime && startTemporal.equals(endTemporal)) {
            endTemporal = endTemporal.plus(1, ChronoUnit.HOURS);
        }
        return Period.between(startTemporal, endTemporal);
    }

}