/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* 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.job;
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.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;
import org.openhab.io.caldav.CalDavEvent;
import org.openhab.io.caldav.EventNotifier;
import org.openhab.io.caldav.internal.CalDavConfig;
import org.openhab.io.caldav.internal.CalDavLoaderImpl;
import org.openhab.io.caldav.internal.EventStorage;
import org.openhab.io.caldav.internal.EventStorage.CalendarRuntime;
import org.openhab.io.caldav.internal.EventStorage.EventContainer;
import org.openhab.io.caldav.internal.Util;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.sardine.DavResource;
import com.github.sardine.Sardine;
import com.github.sardine.impl.SardineException;
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.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.component.CalendarComponent;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.property.Summary;
public class EventReloaderJob implements Job {
public static final String KEY_CONFIG = "config";
private static final Logger log = LoggerFactory.getLogger(EventReloaderJob.class);
private static Map<String, Boolean> cachedEventsLoaded = new ConcurrentHashMap<String, Boolean>();
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
final String config = context.getJobDetail().getJobDataMap().getString(KEY_CONFIG);
CalendarRuntime eventRuntime = EventStorage.getInstance().getEventCache().get(config);
log.debug("running EventReloaderJob for config : {}", config);
// reload cached events (if necessary)
if (!cachedEventsLoaded.containsKey(config)) {
try {
log.debug("reload cached events for config: {}", eventRuntime.getConfig().getKey());
for (File fileCalendarKeys : new File(CalDavLoaderImpl.CACHE_PATH).listFiles()) {
if (!eventRuntime.getConfig().getKey().equals(Util.getFilename(fileCalendarKeys.getName()))) {
log.trace("not our config : {}", Util.getFilename(fileCalendarKeys.getName()));
continue;
}
log.trace("found our config : {}", Util.getFilename(fileCalendarKeys.getName()));
final Collection<File> icsFiles = FileUtils.listFiles(fileCalendarKeys, new String[] { "ics" },
false);
for (File icsFile : icsFiles) {
try {
FileInputStream fis = new FileInputStream(icsFile);
log.debug("loading events from file : {}", 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);
} finally {
cachedEventsLoaded.put(config, true);
}
}
try {
log.debug("loading events for config: " + config);
List<String> oldEventIds = new ArrayList<String>();
for (EventContainer eventContainer : eventRuntime.getEventMap().values()) {
oldEventIds.add(eventContainer.getFilename());
log.debug(
"old eventcontainer -- id : {} -- filename : {} -- calcuntil : {} -- lastchanged : {} -- ishistoric : {}",
eventContainer.getEventId(), eventContainer.getFilename(), eventContainer.getCalculatedUntil(),
eventContainer.getLastChanged(), eventContainer.isHistoricEvent());
if (log.isDebugEnabled()) {
for (int i = 0; i < eventContainer.getEventList().size(); i++) {
CalDavEvent elem = eventContainer.getEventList().get(i);
log.debug("old eventlist contient l'evenement : {} -- deb : {} -- fin : {} -- lastchang {}",
elem.getName(), elem.getStart(), elem.getEnd(), elem.getLastChanged());
}
}
}
loadEvents(eventRuntime, oldEventIds);
// stop all events in oldMap
removeDeletedEvents(config, oldEventIds);
for (EventNotifier notifier : CalDavLoaderImpl.instance.getEventListenerList()) {
try {
notifier.calendarReloaded(config);
} catch (Exception e) {
log.error("error while invoking listener", e);
}
}
// printAllEvents();
// print All scheduled jobs :
if (log.isDebugEnabled()) {
log.debug("jobs scheduled : ");
Scheduler scheduler = CalDavLoaderImpl.instance.getScheduler();
for (String groupName : CalDavLoaderImpl.instance.getScheduler().getJobGroupNames()) {
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) {
String jobName = jobKey.getName();
String jobGroup = jobKey.getGroup();
// get job's trigger
List<Trigger> triggers = (List<Trigger>) scheduler.getTriggersOfJob(jobKey);
Date nextFireTime = triggers.get(0).getNextFireTime();
log.debug("[job] : {} - [groupName] : {} - {}", jobName, jobGroup, nextFireTime);
}
}
}
} catch (SardineException e) {
log.error("error while loading calendar entries: {} ({} - {} )", e.getMessage(), e.getStatusCode(),
e.getResponsePhrase(), e);
throw new JobExecutionException("error while loading calendar entries", e, false);
} catch (Exception e) {
log.error("error while loading calendar entries: {}", e.getMessage(), e);
throw new JobExecutionException("error while loading calendar entries", e, false);
}
}
private synchronized void removeDeletedEvents(String calendarKey, List<String> oldMap) {
final CalendarRuntime eventRuntime = EventStorage.getInstance().getEventCache().get(calendarKey);
for (String filename : oldMap) {
EventContainer eventContainer = eventRuntime.getEventContainerByFilename(filename);
if (eventContainer == null) {
log.error("cannot find event container for filename: {}", filename);
continue;
}
// cancel old jobs
for (String jobId : eventContainer.getTimerMap()) {
try {
String group;
if (jobId.startsWith(CalDavLoaderImpl.JOB_NAME_EVENT_START)) {
group = CalDavLoaderImpl.JOB_NAME_EVENT_START;
} else if (jobId.startsWith(CalDavLoaderImpl.JOB_NAME_EVENT_END)) {
group = CalDavLoaderImpl.JOB_NAME_EVENT_END;
} else {
throw new SchedulerException("unknown job id: " + jobId);
}
boolean deleteJob = CalDavLoaderImpl.instance.getScheduler().deleteJob(JobKey.jobKey(jobId, group));
log.debug("old job ({}) deleted? {}", jobId, deleteJob);
} catch (SchedulerException e) {
log.error("cannot delete job '{}'", jobId);
}
}
eventContainer.getTimerMap().clear();
for (EventNotifier notifier : CalDavLoaderImpl.instance.getEventListenerList()) {
for (CalDavEvent event : eventContainer.getEventList()) {
try {
notifier.eventRemoved(event);
} catch (Exception e) {
log.error("error while invoking listener", e);
}
}
}
ConcurrentHashMap<String, EventContainer> eventContainerMap = eventRuntime.getEventMap();
if (eventContainer != null) {
this.removeFromDisk(eventContainer);
log.debug("remove deleted event: {}", eventContainer.getEventId());
eventContainerMap.remove(eventContainer.getEventId());
}
}
}
private void removeFromDisk(EventContainer eventContainer) {
Util.getCacheFile(eventContainer.getCalendarId(), eventContainer.getFilename()).delete();
}
/**
* 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);
log.trace("before load events : oldeventsid contains : {}", oldEventIds.toString());
for (DavResource resource : list) {
final String filename = Util.getFilename(resource.getName());
try {
if (resource.isDirectory()) {
continue;
}
// an ics file can contain multiple events
// ==> multiple eventcontainers could have the same filename (and different eventid),
// ==>we must not have one of them remaining in oldEventIds var (bad chosen name, cause it's a list of
// oldEventContainers's filename, so with doubles possible)
// or the remaining jobs with this filename will get unscheduled on the "removeDeletedEvents(config,
// oldEventIds)" call (line 136)
oldEventIds.removeAll(Arrays.asList(filename));
// must not be loaded
EventContainer eventContainer = calendarRuntime.getEventContainerByFilename(filename);
final org.joda.time.DateTime lastResourceChangeFS = new org.joda.time.DateTime(resource.getModified());
log.trace("eventContainer found: {}", eventContainer != null);
log.trace("last resource modification: {}", lastResourceChangeFS);
log.trace("last change of already loaded event: {}",
eventContainer != null ? eventContainer.getLastChanged() : null);
if (config.isLastModifiedFileTimeStampValid()) {
if (eventContainer != null && !lastResourceChangeFS.isAfter(eventContainer.getLastChanged())) {
// check if some timers or single (from repeating events) have
// to be created
if (eventContainer.getCalculatedUntil() != null && 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 (calculated until: {})", resource.getName(),
eventContainer.getCalculatedUntil());
continue;
}
if (eventContainer.isHistoricEvent()) {
// no more upcoming events, do nothing
log.trace("skipping resource {}, not changed (historic)", resource.getName());
continue;
}
File icsFile = Util.getCacheFile(config.getKey(), filename);
if (icsFile != null && icsFile.exists()) {
FileInputStream fis = new FileInputStream(icsFile);
this.loadEvents(filename, lastResourceChangeFS, fis, config, oldEventIds, false);
fis.close();
continue;
}
}
}
log.debug("loading resource: {} (FSchangedTS not valid)", resource);
// prepare resource url
URL url = new URL(config.getUrl());
String resourcePath = resource.getPath();
String escapedResource = resource.getName().replaceAll("/", "%2F");
resourcePath = resourcePath.replace(resource.getName(), escapedResource);
url = new URL(url.getProtocol(), url.getHost(), url.getPort(), resourcePath);
InputStream inputStream = sardine.get(url.toString().replaceAll(" ", "%20"));
this.loadEvents(filename, lastResourceChangeFS, inputStream, config, oldEventIds, false);
} catch (ParserException e) {
log.error("error parsing ics file: " + filename, e);
} catch (SardineException e) {
log.error("error reading ics file: " + filename, e);
}
}
log.trace("after load events : oldeventsid contains : {}", oldEventIds.toString());
}
public void loadEvents(String filename, org.joda.time.DateTime lastResourceChangeFS, final InputStream inputStream,
final CalDavConfig config, final List<String> oldEventIds, boolean readFromFile)
throws IOException, ParserException {
CalendarBuilder builder = new CalendarBuilder();
InputStreamReader is = new InputStreamReader(inputStream, config.getCharset());
BufferedReader in = new BufferedReader(is, 50);
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(lastResourceChangeFS);
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) {
log.debug("could not find a VEVENT from calendar build, based on file {}", filename);
// no events inside
if (!readFromFile) {
Util.storeToDisk(config.getKey(), filename, calendar);
}
return;
}
org.joda.time.DateTime lastModifedVEventOverAll = null;
for (CalendarComponent comp : vEventComponents) {
VEvent vEvent = (VEvent) comp;
Summary vEventSummary = vEvent.getSummary();
log.trace("loading event: {}:{}", vEvent.getUid().getValue(), vEventSummary == null ? "(none)" : vEventSummary.getValue());
// fallback, because 'LastModified' in VEvent is optional
org.joda.time.DateTime lastModifedVEvent = lastResourceChangeFS;
if (vEvent.getLastModified() != null) {
lastModifedVEvent = new org.joda.time.DateTime(vEvent.getLastModified().getDateTime());
log.trace("overriding lastmodified from file FS ({}) with event's last-modified property ({})",
lastResourceChangeFS, lastModifedVEvent);
}
if (!config.isLastModifiedFileTimeStampValid()) {
if (lastModifedVEventOverAll == null || lastModifedVEvent.isAfter(lastModifedVEventOverAll)) {
lastModifedVEventOverAll = lastModifedVEvent;
}
if (eventContainer != null && !lastModifedVEvent.isBefore(eventContainer.getLastChanged())) {
// check if some timers or single (from repeating events) have
// to be created
if (eventContainer.getCalculatedUntil() != null && vEventComponents.size() == 1
&& 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 processing. File {} has not changed.", filename);
continue;
}
if (eventContainer.isHistoricEvent()) {
// no more upcoming events, do nothing
log.trace("skipping resource processing. File {} is historic.", filename);
continue;
}
}
}
Period period = new Period(new DateTime(loadFrom.toDate()), new DateTime(loadTo.toDate()));
PeriodList periods = vEvent.calculateRecurrenceSet(period);
periods = periods.normalise();
String eventId = vEvent.getUid().getValue();
final String eventName = vEventSummary == null ? "(none)" : vEventSummary.getValue();
log.debug("Processing event '{}'", eventName);
// 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 occur (historic): {}", eventName);
eventContainer.setHistoricEvent(true);
}
}
else {
log.debug("No periods exist for event '{}'", eventName);
}
// 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 = getDateTime("start", p.getStart(), p.getRangeStart());
org.joda.time.DateTime end = getDateTime("end", p.getEnd(), p.getRangeEnd());
log.trace("Processing period {} - {}", start, end);
CalDavEvent event = new CalDavEvent(eventName, vEvent.getUid().getValue(), config.getKey(), start, end);
event.setLastChanged(lastModifedVEvent);
if (vEvent.getLocation() != null) {
event.setLocation(vEvent.getLocation().getValue());
}
if (vEvent.getDescription() != null) {
event.setContent(vEvent.getDescription().getValue());
}
event.getCategoryList().addAll(readCategory(vEvent));
event.setFilename(filename);
log.trace("adding event: {}", event.getShortName());
eventContainer.getEventList().add(event);
}
}
if (lastModifedVEventOverAll != null && !config.isLastModifiedFileTimeStampValid()) {
eventContainer.setLastChanged(lastModifedVEventOverAll);
log.debug("changing eventcontainer last modified to {}", lastModifedVEventOverAll);
}
CalDavLoaderImpl.instance.addEventToMap(eventContainer, true);
if (!readFromFile) {
Util.storeToDisk(config.getKey(), filename, calendar);
}
}
/**
* Returns a list of categories or an empty list if none found.
*
* @param vEvent
* @return
*/
private List<String> readCategory(VEvent vEvent) {
PropertyList propertyCategoryList = vEvent.getProperties(Property.CATEGORIES);
ArrayList<String> splittedCategoriesToReturn = new ArrayList<String>();
if (propertyCategoryList != null) {
for (int categoriesLineNum = 0; categoriesLineNum < propertyCategoryList.size(); categoriesLineNum++) {
Property propertyCategory = propertyCategoryList.get(categoriesLineNum);
String categories = propertyCategory.getValue();
if (categories != null) {
String[] categoriesSplit = StringUtils.split(categories, ",");
for (String category : categoriesSplit) {
if (!splittedCategoriesToReturn.contains(category)) {
splittedCategoriesToReturn.add(category);
}
}
}
}
}
return splittedCategoriesToReturn;
}
private org.joda.time.DateTime getDateTime(String dateType, DateTime date, Date rangeDate) {
org.joda.time.DateTime start;
if (date.getTimeZone() == null) {
if (date.isUtc()) {
log.trace("{} is without timezone, but UTC", dateType);
start = new org.joda.time.DateTime(rangeDate, DateTimeZone.UTC).toLocalDateTime()
.toDateTime(CalDavLoaderImpl.defaultTimeZone);
} else {
log.trace("{} is without timezone, not UTC", dateType);
start = new LocalDateTime(rangeDate).toDateTime();
}
} else if (DateTimeZone.getAvailableIDs().contains(date.getTimeZone().getID())) {
log.trace("{} is with known timezone: {}", dateType, date.getTimeZone().getID());
start = new org.joda.time.DateTime(rangeDate, DateTimeZone.forID(date.getTimeZone().getID()));
} else {
// unknown timezone
log.trace("{} is with unknown timezone: {}", dateType, date.getTimeZone().getID());
start = new org.joda.time.DateTime(rangeDate, CalDavLoaderImpl.defaultTimeZone);
}
return start;
}
}