org.openhab.io.caldav.internal.CalDavLoaderImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.io.caldav.internal.CalDavLoaderImpl.java

Source

/**
 * Copyright (c) 2010-2015, openHAB.org and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.io.caldav.internal;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;

import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.data.UnfoldingReader;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Period;
import net.fortuna.ical4j.model.PeriodList;
import net.fortuna.ical4j.model.component.CalendarComponent;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.util.CompatibilityHints;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import org.openhab.core.service.AbstractActiveService;
import org.openhab.io.caldav.CalDavEvent;
import org.openhab.io.caldav.CalDavLoader;
import org.openhab.io.caldav.CalDavQuery;
import org.openhab.io.caldav.EventNotifier;
import org.openhab.io.caldav.internal.EventStorage.CalendarRuntime;
import org.openhab.io.caldav.internal.EventStorage.EventContainer;
import org.openhab.io.caldav.internal.job.EventJob;
import org.openhab.io.caldav.internal.job.EventJob.EventTrigger;
import org.openhab.io.caldav.internal.job.EventReloaderJob;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.sardine.DavResource;
import com.github.sardine.Sardine;

/**
 * Loads all events from the configured calDAV servers. This is done with an
 * interval. All interesting events are hold in memory.
 * 
 * @author Robert Delbrck
 * @since 1.8.0
 * 
 */
public class CalDavLoaderImpl extends AbstractActiveService implements ManagedService, CalDavLoader {
    private static final String JOB_NAME_EVENT_RELOADER = "event-reloader";
    private static final String JOB_NAME_EVENT_START = "event-start";
    private static final String JOB_NAME_EVENT_END = "event-end";
    private static final String PROP_RELOAD_INTERVAL = "reloadInterval";
    private static final String PROP_PRELOAD_TIME = "preloadTime";
    private static final String PROP_HISTORIC_LOAD_TIME = "historicLoadTime";
    private static final String PROP_URL = "url";
    private static final String PROP_PASSWORD = "password";
    private static final String PROP_USERNAME = "username";
    private static final String PROP_TIMEZONE = "timeZone";
    private static final String PROP_DISABLE_CERTIFICATE_VERIFICATION = "disableCertificateVerification";
    private DateTimeZone defaultTimeZone = DateTimeZone.getDefault();

    private static final Logger log = LoggerFactory.getLogger(CalDavLoaderImpl.class);
    public static final String CACHE_PATH = "etc/caldav";

    private ScheduledExecutorService execService;
    private List<EventNotifier> eventListenerList = new ArrayList<EventNotifier>();
    private final Scheduler scheduler;

    public static CalDavLoaderImpl INSTANCE;

    public CalDavLoaderImpl() {
        if (INSTANCE != null) {
            throw new IllegalStateException("something went wrong, the loader service should be singleton");
        }
        INSTANCE = this;

        try {
            scheduler = new StdSchedulerFactory().getScheduler();
            scheduler.clear();
        } catch (SchedulerException e) {
            log.error("cannot get job-scheduler", e);
            throw new IllegalStateException("cannot get job-scheduler", e);
        }

    }

    @Override
    public void updated(Dictionary<String, ?> config) throws ConfigurationException {
        if (config != null) {
            CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true);

            // just temporary
            Map<String, CalDavConfig> configMap = new HashMap<String, CalDavConfig>();

            Enumeration<String> iter = config.keys();
            while (iter.hasMoreElements()) {
                String key = iter.nextElement();
                log.trace("configuration parameter: " + key);
                if (key.equals("service.pid")) {
                    continue;
                } else if (key.equals(PROP_TIMEZONE)) {
                    log.debug("overriding default timezone {} with {}", defaultTimeZone, config.get(key));
                    defaultTimeZone = DateTimeZone.forID(config.get(key) + "");
                    if (defaultTimeZone == null) {
                        throw new ConfigurationException(PROP_TIMEZONE,
                                "invalid timezone value: " + config.get(key));
                    }
                    log.debug("found timeZone: {}", defaultTimeZone);
                    continue;
                }
                String[] keys = key.split(":");
                if (keys.length != 2) {
                    throw new ConfigurationException(key, "unknown identifier");
                }
                String id = keys[0];
                String paramKey = keys[1];
                CalDavConfig calDavConfig = configMap.get(id);
                if (calDavConfig == null) {
                    calDavConfig = new CalDavConfig();
                    configMap.put(id, calDavConfig);
                }
                String value = config.get(key) + "";

                calDavConfig.setKey(id);
                if (paramKey.equals(PROP_USERNAME)) {
                    calDavConfig.setUsername(value);
                } else if (paramKey.equals(PROP_PASSWORD)) {
                    calDavConfig.setPassword(value);
                } else if (paramKey.equals(PROP_URL)) {
                    calDavConfig.setUrl(value);
                } else if (paramKey.equals(PROP_RELOAD_INTERVAL)) {
                    calDavConfig.setReloadMinutes(Integer.parseInt(value));
                } else if (paramKey.equals(PROP_PRELOAD_TIME)) {
                    calDavConfig.setPreloadMinutes(Integer.parseInt(value));
                } else if (paramKey.equals(PROP_HISTORIC_LOAD_TIME)) {
                    calDavConfig.setHistoricLoadMinutes(Integer.parseInt(value));
                } else if (paramKey.equals(PROP_DISABLE_CERTIFICATE_VERIFICATION)) {
                    calDavConfig.setDisableCertificateVerification(BooleanUtils.toBoolean(value));
                }
            }

            // verify if all required parameters are set
            for (String id : configMap.keySet()) {
                if (configMap.get(id).getUrl() == null) {
                    throw new ConfigurationException(PROP_URL, PROP_URL + " must be set");
                }
                if (configMap.get(id).getUsername() == null) {
                    throw new ConfigurationException(PROP_USERNAME, PROP_USERNAME + " must be set");
                }
                if (configMap.get(id).getPassword() == null) {
                    throw new ConfigurationException(PROP_PASSWORD, PROP_PASSWORD + " must be set");
                }
                log.trace("config for id '{}': {}", id, configMap.get(id));
            }

            // initialize event cache
            for (CalDavConfig calDavConfig : configMap.values()) {
                final CalendarRuntime eventRuntime = new CalendarRuntime();
                eventRuntime.setConfig(calDavConfig);
                File cachePath = Util.getCachePath(calDavConfig.getKey());
                if (!cachePath.exists() && !cachePath.mkdirs()) {
                    log.error(
                            "cannot create directory (" + CACHE_PATH + ") for calendar caching (missing rights?)");
                    continue;
                }
                EventStorage.getInstance().getEventCache().put(calDavConfig.getKey(), eventRuntime);
            }

            setProperlyConfigured(true);
            this.startLoading();
        }
    }

    public List<EventNotifier> getEventListenerList() {
        return eventListenerList;
    }

    public Scheduler getScheduler() {
        return scheduler;
    }

    public void addListener(EventNotifier notifier) {
        this.eventListenerList.add(notifier);

        // notify for missing changes
        for (CalendarRuntime calendarRuntime : EventStorage.getInstance().getEventCache().values()) {
            for (EventContainer eventContainer : calendarRuntime.getEventMap().values()) {
                for (CalDavEvent event : eventContainer.getEventList()) {
                    notifier.eventLoaded(event);
                }
            }
        }

    }

    public void removeListener(EventNotifier notifier) {
        this.eventListenerList.remove(notifier);
    }

    private synchronized void addEventToMap(EventContainer eventContainer, boolean createTimer) {
        CalendarRuntime calendarRuntime = EventStorage.getInstance().getEventCache()
                .get(eventContainer.getCalendarId());

        ConcurrentHashMap<String, EventContainer> eventContainerMap = calendarRuntime.getEventMap();

        if (eventContainerMap.containsKey(eventContainer.getEventId())) {
            EventContainer eventContainerOld = eventContainerMap.get(eventContainer.getEventId());
            // event is already in map
            if (eventContainer.getLastChanged().isAfter(eventContainerOld.getLastChanged())) {
                log.debug("event is already in event map and newer -> delete the old one, reschedule timer");
                // cancel old jobs
                for (String timerKey : eventContainerOld.getTimerMap()) {
                    try {
                        this.scheduler.deleteJob(JobKey.jobKey(timerKey));
                    } catch (SchedulerException e) {
                        log.error("cannot cancel event with job-id: " + timerKey, e);
                    }
                }
                eventContainerOld.getTimerMap().clear();

                // override event
                eventContainerMap.put(eventContainer.getEventId(), eventContainer);

                for (EventNotifier notifier : eventListenerList) {
                    for (CalDavEvent event : eventContainerOld.getEventList()) {
                        log.trace("notify listener... {}", notifier);
                        try {
                            notifier.eventRemoved(event);
                        } catch (Exception e) {
                            log.error("error while invoking listener", e);
                        }
                    }
                }
                for (EventNotifier notifier : eventListenerList) {
                    for (CalDavEvent event : eventContainer.getEventList()) {
                        log.trace("notify listener... {}", notifier);
                        try {
                            notifier.eventLoaded(event);
                        } catch (Exception e) {
                            log.error("error while invoking listener", e);
                        }
                    }
                }

                if (createTimer) {
                    int index = 0;
                    for (CalDavEvent event : eventContainer.getEventList()) {
                        if (event.getEnd().isAfterNow()) {
                            try {
                                createJob(eventContainer, event, index);
                            } catch (SchedulerException e) {
                                log.error("cannot create jobs for event: " + event.getShortName());
                            }
                        }
                        index++;
                    }
                }
            } else {
                // event is already in map and not updated, ignoring
            }
        } else {
            // event is new
            eventContainerMap.put(eventContainer.getEventId(), eventContainer);
            log.trace("listeners for events: {}", eventListenerList.size());
            for (EventNotifier notifier : eventListenerList) {
                for (CalDavEvent event : eventContainer.getEventList()) {
                    log.trace("notify listener... {}", notifier);
                    try {
                        notifier.eventLoaded(event);
                    } catch (Exception e) {
                        log.error("error while invoking listener", e);
                    }
                }
            }
            if (createTimer) {
                int index = 0;
                for (CalDavEvent event : eventContainer.getEventList()) {
                    if (event.getEnd().isAfterNow()) {
                        try {
                            createJob(eventContainer, event, index);
                        } catch (SchedulerException e) {
                            log.error("cannot create jobs for event: " + event.getShortName());
                        }
                    }
                    index++;
                }
            }
        }
    }

    private synchronized void createJob(final EventContainer eventContainer, final CalDavEvent event,
            final int index) throws SchedulerException {
        final String triggerStart = JOB_NAME_EVENT_START + "-" + event.getShortName();
        Date startDate = event.getStart().toDate();
        JobDetail jobStart = JobBuilder.newJob().ofType(EventJob.class)
                .usingJobData(EventJob.KEY_CONFIG, eventContainer.getCalendarId())
                .usingJobData(EventJob.KEY_EVENT, eventContainer.getEventId())
                .usingJobData(EventJob.KEY_REC_INDEX, index)
                .usingJobData(EventJob.KEY_EVENT_TRIGGER, EventTrigger.BEGIN.name()).build();
        Trigger jobTriggerStart = TriggerBuilder.newTrigger().withIdentity(triggerStart, JOB_NAME_EVENT_START)
                .startAt(startDate).build();
        this.scheduler.scheduleJob(jobStart, jobTriggerStart);

        eventContainer.getTimerMap().add(triggerStart);
        log.debug("begin timer scheduled for event '{}' @ {}", event.getShortName(), startDate);

        final String triggerEnd = JOB_NAME_EVENT_END + "-" + event.getShortName();
        Date endDate = event.getEnd().toDate();
        JobDetail jobEnd = JobBuilder.newJob().ofType(EventJob.class)
                .usingJobData(EventJob.KEY_CONFIG, eventContainer.getCalendarId())
                .usingJobData(EventJob.KEY_EVENT, eventContainer.getEventId())
                .usingJobData(EventJob.KEY_REC_INDEX, index)
                .usingJobData(EventJob.KEY_EVENT_TRIGGER, EventTrigger.END.name()).build();
        Trigger jobTriggerEnd = TriggerBuilder.newTrigger().withIdentity(triggerEnd, JOB_NAME_EVENT_END)
                .startAt(endDate).build();
        this.scheduler.scheduleJob(jobEnd, jobTriggerEnd);
        eventContainer.getTimerMap().add(triggerEnd);
        log.debug("end timer scheduled for event '{}' @ {}", event.getShortName(), endDate);
    }

    /**
     * all events which are available must be removed from the oldEventIds list
     * 
     * @param calendarRuntime
     * @param oldEventIds
     * @throws IOException
     * @throws ParserException
     */
    public synchronized void loadEvents(final CalendarRuntime calendarRuntime, final List<String> oldEventIds)
            throws IOException, ParserException {
        CalDavConfig config = calendarRuntime.getConfig();

        Sardine sardine = Util.getConnection(config);

        List<DavResource> list = sardine.list(config.getUrl(), 1, false);

        for (DavResource resource : list) {
            if (resource.isDirectory()) {
                continue;
            }

            final String filename = Util.getFilename(resource.getName());
            oldEventIds.remove(filename);

            // must not be loaded
            EventContainer eventContainer = calendarRuntime.getEventContainerByFilename(filename);
            final org.joda.time.DateTime lastResourceChange = new org.joda.time.DateTime(resource.getModified());

            log.trace("eventContainer found: {}", eventContainer != null);
            log.trace("last resource modification: {}", lastResourceChange);
            log.trace("last change of already loaded event: {}",
                    eventContainer != null ? eventContainer.getLastChanged() : null);
            if (eventContainer != null && !lastResourceChange.isAfter(eventContainer.getLastChanged())) {
                // check if some timers or single (from repeating events) have
                // to be created
                if (eventContainer.getCalculatedUntil()
                        .isAfter(org.joda.time.DateTime.now().plusMinutes(config.getReloadMinutes()))) {
                    // the event is calculated as long as the next reload
                    // interval can handle this
                    log.trace("skipping resource {}, not changed", resource.getName());
                    continue;
                }

                if (eventContainer.isHistoricEvent()) {
                    // no more upcoming events, do nothing
                    log.trace("skipping resource {}, not changed", resource.getName());
                    continue;
                }

                File icsFile = Util.getCacheFile(config.getKey(), filename);
                if (icsFile != null && icsFile.exists()) {
                    FileInputStream fis = new FileInputStream(icsFile);
                    this.loadEvents(filename, lastResourceChange, fis, config, oldEventIds, false);
                    fis.close();
                    continue;
                }
            }

            log.debug("loading resource: {}", resource);

            URL url = new URL(config.getUrl());
            url = new URL(url.getProtocol(), url.getHost(), url.getPort(), resource.getPath());

            InputStream inputStream = sardine.get(url.toString().replaceAll(" ", "%20"));

            try {
                this.loadEvents(filename, lastResourceChange, inputStream, config, oldEventIds, false);
            } catch (ParserException e) {
                log.error("cannot load calendar entry: " + filename, e);
            }
        }
    }

    private void loadEvents(String filename, org.joda.time.DateTime lastChanged, final InputStream inputStream,
            final CalDavConfig config, final List<String> oldEventIds, boolean readFromFile)
            throws IOException, ParserException {
        CalendarBuilder builder = new CalendarBuilder();
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream), 50);
        //      String calContent = IOUtils.toString(in);
        //      calContent = calContent.replace("\r", "\r\n");
        //      log.info(calContent);
        //      in.close();

        final UnfoldingReader uin = new UnfoldingReader(in, 50, true);
        Calendar calendar = builder.build(uin);
        uin.close();
        log.trace("calendar: {}", calendar);

        EventContainer eventContainer = new EventContainer(config.getKey());
        eventContainer.setFilename(filename);
        eventContainer.setLastChanged(lastChanged);

        org.joda.time.DateTime loadFrom = org.joda.time.DateTime.now()
                .minusMinutes(config.getHistoricLoadMinutes());
        org.joda.time.DateTime loadTo = org.joda.time.DateTime.now().plusMinutes(config.getPreloadMinutes());

        final ComponentList<CalendarComponent> vEventComponents = calendar.getComponents(Component.VEVENT);
        if (vEventComponents.size() == 0) {
            // no events inside
            if (!readFromFile) {
                Util.storeToDisk(config.getKey(), filename, calendar);
            }
            return;
        }
        for (CalendarComponent comp : vEventComponents) {
            VEvent vEvent = (VEvent) comp;
            log.trace("loading event: " + vEvent.getUid().getValue() + ":" + vEvent.getSummary().getValue());

            PeriodList periods = vEvent.calculateRecurrenceSet(
                    new Period(new DateTime(loadFrom.toDate()), new DateTime(loadTo.toDate())));

            String eventId = vEvent.getUid().getValue();
            final String eventName = vEvent.getSummary().getValue();

            // no more upcoming events
            if (periods.size() > 0) {
                if (vEvent.getConsumedTime(new net.fortuna.ical4j.model.Date(),
                        new net.fortuna.ical4j.model.Date(org.joda.time.DateTime.now().plusYears(10).getMillis()))
                        .size() == 0) {
                    log.trace("event will never be occur (historic): {}", eventName);
                    eventContainer.setHistoricEvent(true);
                }
            }

            // expecting this is for every vEvent inside a calendar equals
            eventContainer.setEventId(eventId);

            eventContainer.setCalculatedUntil(loadTo);

            for (Period p : periods) {
                org.joda.time.DateTime start = null;
                org.joda.time.DateTime end = null;

                if (p.getStart().getTimeZone() == null) {
                    if (p.getStart().isUtc()) {
                        log.trace("start is without timezone, but UTC");
                        start = new org.joda.time.DateTime(p.getRangeStart(), DateTimeZone.UTC).toLocalDateTime()
                                .toDateTime(defaultTimeZone);
                    } else {
                        log.trace("start is without timezone, not UTC");
                        start = new LocalDateTime(p.getRangeStart()).toDateTime();
                    }
                } else if (DateTimeZone.getAvailableIDs().contains(p.getStart().getTimeZone().getID())) {
                    log.trace("start is with known timezone: {}", p.getStart().getTimeZone().getID());
                    start = new org.joda.time.DateTime(p.getRangeStart(),
                            DateTimeZone.forID(p.getStart().getTimeZone().getID()));
                } else {
                    // unknown timezone
                    log.trace("start is with unknown timezone: {}", p.getStart().getTimeZone().getID());
                    start = new org.joda.time.DateTime(p.getRangeStart(), defaultTimeZone);
                }

                if (p.getEnd().getTimeZone() == null) {
                    if (p.getStart().isUtc()) {
                        end = new org.joda.time.DateTime(p.getRangeEnd(), DateTimeZone.UTC).toLocalDateTime()
                                .toDateTime(defaultTimeZone);
                    } else {
                        end = new LocalDateTime(p.getRangeEnd()).toDateTime();
                    }
                } else if (DateTimeZone.getAvailableIDs().contains(p.getEnd().getTimeZone().getID())) {
                    end = new org.joda.time.DateTime(p.getRangeEnd(),
                            DateTimeZone.forID(p.getEnd().getTimeZone().getID()));
                } else {
                    // unknown timezone
                    end = new org.joda.time.DateTime(p.getRangeEnd(), defaultTimeZone);
                }

                CalDavEvent event = new CalDavEvent(eventName, vEvent.getUid().getValue(), config.getKey(), start,
                        end);
                event.setLastChanged(lastChanged);
                if (vEvent.getLocation() != null) {
                    event.setLocation(vEvent.getLocation().getValue());
                }
                if (vEvent.getDescription() != null) {
                    event.setContent(vEvent.getDescription().getValue());
                }
                event.setFilename(filename);
                log.trace("adding event: " + event.getShortName());
                eventContainer.getEventList().add(event);

            }
        }
        addEventToMap(eventContainer, true);
        if (!readFromFile) {
            Util.storeToDisk(config.getKey(), filename, calendar);
        }
    }

    public void startLoading() {
        if (execService != null) {
            return;
        }
        log.trace("starting execution...");

        for (final CalendarRuntime eventRuntime : EventStorage.getInstance().getEventCache().values()) {
            try {
                log.debug("reload cached events for config: {}", eventRuntime.getConfig().getKey());
                for (File fileCalendarKeys : new File(CACHE_PATH).listFiles()) {
                    if (!eventRuntime.getConfig().getKey().equals(Util.getFilename(fileCalendarKeys.getName()))) {
                        continue;
                    }
                    final Collection<File> icsFiles = FileUtils.listFiles(fileCalendarKeys, new String[] { "ics" },
                            false);
                    for (File icsFile : icsFiles) {
                        try {
                            FileInputStream fis = new FileInputStream(icsFile);
                            loadEvents(Util.getFilename(icsFile.getAbsolutePath()),
                                    new org.joda.time.DateTime(icsFile.lastModified()), fis,
                                    eventRuntime.getConfig(), new ArrayList<String>(), true);
                        } catch (IOException e) {
                            log.error("cannot load events for file: " + icsFile, e);
                        } catch (ParserException e) {
                            log.error("cannot load events for file: " + icsFile, e);
                        }
                    }
                    break;
                }
            } catch (Throwable e) {
                log.error("cannot load events", e);
            }

            try {
                JobDetail job = JobBuilder.newJob().ofType(EventReloaderJob.class)
                        .usingJobData(EventReloaderJob.KEY_CONFIG, eventRuntime.getConfig().getKey()).build();
                SimpleTrigger jobTrigger = TriggerBuilder.newTrigger()
                        .withIdentity(JOB_NAME_EVENT_RELOADER + "-" + eventRuntime.getConfig().getKey(),
                                JOB_NAME_EVENT_RELOADER)
                        .startAt(new Date()).withSchedule(SimpleScheduleBuilder
                                .repeatMinutelyForever(eventRuntime.getConfig().getReloadMinutes()))
                        .build();
                this.scheduler.scheduleJob(job, jobTrigger);
            } catch (SchedulerException e) {
                log.error("cannot schedule calendar-reloader", e);
            }
        }
    }

    @Override
    protected void execute() {

    }

    @Override
    protected long getRefreshInterval() {
        return 1000;
    }

    @Override
    protected String getName() {
        return "CalDav Loader";
    }

    @Override
    public void addEvent(CalDavEvent calDavEvent) {
        final CalendarRuntime calendarRuntime = EventStorage.getInstance().getEventCache()
                .get(calDavEvent.getCalendarId());
        CalDavConfig config = calendarRuntime.getConfig();
        if (config == null) {
            log.error("cannot find config for calendar id: {}", calDavEvent.getCalendarId());
        }
        Sardine sardine = Util.getConnection(config);

        Calendar calendar = Util.createCalendar(calDavEvent, this.defaultTimeZone);

        try {
            final String fullIcsFile = config.getUrl() + "/" + calDavEvent.getFilename() + ".ics";
            if (calendarRuntime.getEventContainerByFilename(calDavEvent.getFilename()) != null) {
                log.debug("event will be updated: {}", fullIcsFile);
                try {
                    sardine.delete(fullIcsFile);
                } catch (IOException e) {
                    log.error("cannot remove old ics file: {}", fullIcsFile);
                }
            } else {
                log.debug("event is new: {}", fullIcsFile);
            }

            sardine.put(fullIcsFile, calendar.toString().getBytes("UTF-8"));

            EventContainer eventContainer = new EventContainer(calDavEvent.getCalendarId());
            eventContainer.setEventId(calDavEvent.getId());
            eventContainer.setFilename(Util.getFilename(calDavEvent.getFilename()));
            eventContainer.getEventList().add(calDavEvent);
            eventContainer.setLastChanged(calDavEvent.getLastChanged());
            this.addEventToMap(eventContainer, false);
        } catch (UnsupportedEncodingException e) {
            log.error("cannot write event", e);
        } catch (IOException e) {
            log.error("cannot write event", e);
        }
    }

    @Override
    public List<CalDavEvent> getEvents(final CalDavQuery query) {
        log.trace("quering events for filter: {}", query);
        final ArrayList<CalDavEvent> eventList = new ArrayList<CalDavEvent>();

        if (query.getCalendarIds() != null) {
            for (String calendarId : query.getCalendarIds()) {
                final CalendarRuntime eventRuntime = EventStorage.getInstance().getEventCache().get(calendarId);
                if (eventRuntime == null) {
                    log.debug("calendar id {} not found", calendarId);
                    continue;
                }

                for (EventContainer eventContainer : eventRuntime.getEventMap().values()) {
                    for (CalDavEvent calDavEvent : eventContainer.getEventList()) {
                        if (query.getFrom() != null) {
                            if (calDavEvent.getEnd().isBefore(query.getFrom())) {
                                continue;
                            }
                        }
                        if (query.getTo() != null) {
                            if (calDavEvent.getStart().isAfter(query.getTo())) {
                                continue;
                            }
                        }
                        eventList.add(calDavEvent);
                    }
                }
            }
        }

        if (query.getSort() != null) {
            Collections.sort(eventList, new Comparator<CalDavEvent>() {
                @Override
                public int compare(CalDavEvent arg0, CalDavEvent arg1) {
                    if (query.getSort().equals(CalDavQuery.Sort.ASCENDING)) {
                        return (int) (arg0.getStart().compareTo(arg1.getStart()));
                    } else if (query.getSort().equals(CalDavQuery.Sort.DESCENDING)) {
                        return (int) (arg1.getStart().compareTo(arg0.getStart()));
                    } else {
                        return 0;
                    }

                }
            });
        }

        log.debug("return event list for {} with {} entries", query, eventList.size());
        return eventList;
    }

}