Java tutorial
/** * 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 static org.quartz.JobBuilder.newJob; import static org.quartz.TriggerBuilder.newTrigger; import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Dictionary; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.meta.When; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ComponentList; import net.fortuna.ical4j.model.component.VEvent; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.protocol.Protocol; import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.LongRange; import org.joda.time.DateTime; import org.openhab.core.service.AbstractActiveService; import org.openhab.io.caldav.internal.util.ExecuteCommandJob; import org.openhab.io.caldav.internal.util.TimeRangeCalendar; import org.openhab.io.caldav.util.EasySSLProtocolSocketFactory; import org.osaf.caldav4j.CalDAVCollection; import org.osaf.caldav4j.CalDAVConstants; import org.osaf.caldav4j.exceptions.CalDAV4JException; import org.osaf.caldav4j.methods.CalDAV4JMethodFactory; import org.osaf.caldav4j.methods.HttpClient; import org.osaf.caldav4j.model.request.CalendarQuery; import org.osaf.caldav4j.util.GenerateQuery; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.quartz.Job; import org.quartz.JobDetail; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Service which downloads Calendar events, parses their content and creates * Quartz-jobs and triggers out of them. * * this code is derived from GCal implementation written by Thomas.Eichstaedt-Engelen * @author Thomas.Schmidt * @since 1.8.0 */ public class CalDavEventDownloader extends AbstractActiveService implements ManagedService { private static final String CALDAV_SCHEDULER_GROUP = "caldav"; private static final Logger logger = LoggerFactory.getLogger(CalDavEventDownloader.class); private static String host = ""; private static int port = 0; private static String username = ""; private static String password = ""; private static String url = ""; private static boolean tls = true; private static boolean strictTls = true; /** holds the current refresh interval, default to 900000ms (15 minutes) */ public static int refreshInterval = 900000; /** holds the local quartz scheduler instance */ private Scheduler scheduler; /** * RegEx to extract the start and end commands from the Calendar-Event content. * (<code>'start\s*?\{(.*?)\}\s*end\s*?\{(.*?)\}\s*'</code>) */ private static final Pattern EXTRACT_STARTEND_CONTENT = Pattern .compile("start\\s*?\\{(.*?)\\}\\s*end\\s*?\\{(.*?)\\}\\s*", Pattern.DOTALL); /** * RegEx to extract the modified by command from the Calendar-Event content. * (<code>'(.*?)modified by\s*?\{(.*?)\}.*'</code>) */ private static final Pattern EXTRACT_MODIFIEDBY_CONTENT = Pattern.compile("(.*?)modified by\\s*?\\{(.*?)\\}.*", Pattern.DOTALL); @Override protected long getRefreshInterval() { return refreshInterval; } @Override protected String getName() { return "CalDav Calender Event-Downloader"; } @Override public void activate() { logger.debug("activate CalDavEventDownloader"); try { scheduler = StdSchedulerFactory.getDefaultScheduler(); super.activate(); } catch (SchedulerException se) { logger.error("initializing scheduler throws exception", se); } } /** * @{inheritDoc} */ @Override protected void execute() { List<VEvent> eventList = downloadEventFeed(); if (!eventList.isEmpty()) { logger.debug("found {} calendar events to process", eventList.size()); try { if (scheduler.isShutdown()) { logger.warn("Scheduler has been shut down - probably due to exceptions?"); } cleanJobs(); processEntries(eventList); } catch (SchedulerException se) { logger.error("scheduling jobs throws exception", se); } } else { logger.debug("caldav contains no events ..."); } } /** * Connects to Caldav-Calendar Service and downloads the specified Calendar * <code>url</code>, <code>username</code>, <code>host</code>, <code>port</code> and <code>password</code> are taken * from the corresponding config parameter in <code>openhab.cfg</code>. * The same is for ssl/tls settings <code>ssl</code> and * * @return the corresponding Calendar-Feed or <code>null</code> if an error * occurs. <i>Note:</i> We do only return events if their startTime lies between * <code>now</code> and <code>now + 2 * refreshInterval</code> to reduce * the amount of events to process. */ public static List<VEvent> downloadEventFeed() { List<VEvent> eventList = new ArrayList<VEvent>(); if (tls && (!strictTls)) { ProtocolSocketFactory socketFactory = new EasySSLProtocolSocketFactory(); Protocol https = new Protocol("https", socketFactory, 443); Protocol.registerProtocol("https", https); } HttpClient httpClient = new HttpClient(); httpClient.getHostConfiguration().setHost(host, port, tls ? "https" : "http"); UsernamePasswordCredentials httpCredentials = new UsernamePasswordCredentials(username, password); httpClient.getState().setCredentials(AuthScope.ANY, httpCredentials); httpClient.getParams().setAuthenticationPreemptive(true); GenerateQuery gq = new GenerateQuery(); TimeZone timeZone = TimeZone.getDefault(); java.util.Calendar calStartSearch = new GregorianCalendar(timeZone); // calStartSearch.add(java.util.Calendar.DAY_OF_MONTH, -1); java.util.Calendar calEndSearch = new GregorianCalendar(); calEndSearch.add(java.util.Calendar.SECOND, (2 * refreshInterval / 1000)); SimpleDateFormat dateSearchFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); dateSearchFormat.setTimeZone(TimeZone.getTimeZone("UTC")); try { logger.debug("getting CalDav entries with filter \"VEVENT [{};{}] : STATUS!=CANCELLED\"", dateSearchFormat.format(calStartSearch.getTime()), dateSearchFormat.format(calEndSearch.getTime())); gq.setFilter("VEVENT [" + dateSearchFormat.format(calStartSearch.getTime()) + ";" + dateSearchFormat.format(calEndSearch.getTime()) + "] : STATUS!=CANCELLED"); CalendarQuery calendarQuery = gq.generate(); calendarQuery.validate(); // Document document = calendarQuery.createNewDocument(XMLUtils.getDOMImplementation()); // XMLUtils.toPrettyXML(document); // System.out.println(XMLUtils.toPrettyXML(document)); CalDAVCollection collection = new CalDAVCollection(url, (HostConfiguration) httpClient.getHostConfiguration().clone(), new CalDAV4JMethodFactory(), CalDAVConstants.PROC_ID_DEFAULT); List<Calendar> calendars = collection.queryCalendars(httpClient, calendarQuery); for (Calendar calendar : calendars) { ComponentList componentList = calendar.getComponents().getComponents(Component.VEVENT); Iterator<?> eventIterator = componentList.iterator(); while (eventIterator.hasNext()) { VEvent ve = (VEvent) eventIterator.next(); eventList.add(ve); logger.debug("Got CalDav entry <{}>, with command <{}> for time interval: {} to {} ", ve.getSummary(), ve.getDescription(), ve.getStartDate(), ve.getEndDate()); } } } catch (CalDAV4JException ce) { logger.error("scheduling jobs throws exception", ce); } return eventList; } /** * Delete all {@link Job}s of the group <code>GCAL_SCHEDULER_GROUP</code> * * @throws SchedulerException if there is an internal Scheduler error. */ private void cleanJobs() throws SchedulerException { Set<JobKey> jobKeys = scheduler.getJobKeys(jobGroupEquals(CALDAV_SCHEDULER_GROUP)); scheduler.deleteJobs(new ArrayList<JobKey>(jobKeys)); } /** * <p> * Iterates through <code>entries</code>, extracts the event content and * creates quartz calendars, jobs and corresponding triggers for each event. * </p> * <p> * The following steps are done at event processing: * <ul> * <li>find events with empty content</li> * <li>create a {@link TimeRangeCalendar} for each event (unique by title) and add a TimeRange for each {@link When}</li> * <li>add each {@link TimeRangeCalendar} to the {@link Scheduler}</li> * <li>find events with content</li> * <li>add a Job with the corresponding Triggers for each event</li> * </ul> * * @param entries the GCalendar events to create quart jobs for. * @throws SchedulerException if there is an internal Scheduler error. */ private void processEntries(List<VEvent> entries) throws SchedulerException { Map<String, TimeRangeCalendar> calendarCache = new HashMap<String, TimeRangeCalendar>(); // find all events with empty content - these events are taken to modify // the scheduler if (entries == null) return; for (VEvent event : entries) { String eventContent = ""; String eventTitle = ""; if (event.getDescription() != null) eventContent = event.getDescription().getValue(); if (event.getSummary() != null) eventTitle = event.getSummary().getValue(); if (StringUtils.isBlank(eventContent)) { logger.debug( "found event '{}' with no content, add this event to the excluded " + "TimeRangesCalendar - this event could be referenced by the modifiedBy clause", eventTitle); if (!calendarCache.containsKey(eventTitle)) { calendarCache.put(eventTitle, new TimeRangeCalendar()); } TimeRangeCalendar timeRangeCalendar = calendarCache.get(eventTitle); timeRangeCalendar.addTimeRange(new LongRange(event.getStartDate().getDate().getTime(), event.getEndDate().getDate().getTime())); } } // add all calendars to the Scheduler an rebase all existing Triggers // the calendars has to be added first, to schedule Triggers successfully for (Entry<String, TimeRangeCalendar> entry : calendarCache.entrySet()) { scheduler.addCalendar(entry.getKey(), entry.getValue(), true, true); } // now we process all events with content for (VEvent event : entries) { String eventContent = ""; String eventTitle = ""; if (event.getDescription() != null) eventContent = event.getDescription().getValue(); if (event.getSummary() != null) eventTitle = event.getSummary().getValue(); if (StringUtils.isNotBlank(eventContent)) { CalendarEventContent cec = parseEventContent(eventContent); String modifiedByEvent = null; if (calendarCache.containsKey(cec.modifiedByEvent)) { modifiedByEvent = cec.modifiedByEvent; } JobDetail startJob = createJob(cec.startCommands, event, true); boolean triggersCreated = createTriggerAndSchedule(startJob, event, modifiedByEvent, true); if (triggersCreated) { logger.info("created new startJob '{}' with details '{}'", eventTitle, createJobInfo(event, startJob)); } // do only create end-jobs if there are end-commands ... if (StringUtils.isNotBlank(cec.endCommands)) { JobDetail endJob = createJob(cec.endCommands, event, false); triggersCreated = createTriggerAndSchedule(endJob, event, modifiedByEvent, false); if (triggersCreated) { logger.info("created new endJob '{}' with details '{}'", eventTitle, createJobInfo(event, endJob)); } } } } } /** * <p> * Extracts start, end and modified by-commands from <code>content</code>. * Start-Commands will be executed at start-time and End-Commands will be * executed at end-time of the calendar-event. The modified-by command defines * the name of special event which disables the created Job temporarily. * </p><p> * If the RegExp <code>EXTRACT_STARTEND_CONTENT</code> doen't match the * complete content is taken as set of Start-Commands. * </p> * * @param content the set of Start- and End-Commands * @return the parsed event content */ protected CalendarEventContent parseEventContent(String content) { CalendarEventContent eventContent = new CalendarEventContent(); String commandContent; Matcher modifiedByMatcher = EXTRACT_MODIFIEDBY_CONTENT.matcher(content); if (modifiedByMatcher.find()) { commandContent = modifiedByMatcher.group(1); eventContent.modifiedByEvent = StringUtils.trimToEmpty(modifiedByMatcher.group(2)); } else { commandContent = content; } Matcher startEndMatcher = EXTRACT_STARTEND_CONTENT.matcher(commandContent); if (startEndMatcher.find()) { eventContent.startCommands = StringUtils.trimToEmpty(startEndMatcher.group(1)); eventContent.endCommands = StringUtils.trimToEmpty(startEndMatcher.group(2)); } else { eventContent.startCommands = StringUtils.trimToEmpty(commandContent); logger.debug( "given event content doesn't match regular expression to " + "extract start-, end commands - using whole content as startCommand ({})", commandContent); } return eventContent; } /** * Creates a new quartz-job with jobData <code>content</code> in the scheduler * group <code>GCAL_SCHEDULER_GROUP</code> if <code>content</code> is not * blank. * * @param content the set of commands to be executed by the * {@link ExecuteCommandJob} later on * @param event * @param isStartEvent indicator to identify whether this trigger will be * triggering a start or an end command. * * @return the {@link JobDetail}-object to be used at further processing */ protected JobDetail createJob(String content, VEvent event, boolean isStartEvent) { String jobIdentity = event.getUid() + (isStartEvent ? "_start" : "_end"); if (StringUtils.isBlank(content)) { logger.debug("content of job '" + jobIdentity + "' is empty -> no task will be created!"); return null; } JobDetail job = newJob(ExecuteCommandJob.class) .usingJobData(ExecuteCommandJob.JOB_DATA_CONTENT_KEY, content) .withIdentity(jobIdentity, CALDAV_SCHEDULER_GROUP).build(); return job; } /** * Creates a set quartz-triggers for <code>job</code>. For each {@link When} * object of <code>event</code> a new trigger is created. That is the case * in recurring events where gcal creates one event (with one unique IcalUID) * and a set of {@link When}-object for each occurrence. * * @param job the {@link Job} to create triggers for * @param event the {@link CalendarEventEntry} to read the {@link When}-objects * from * @param modifiedByEvent defines the name of an event which modifies the * schedule of the new Trigger * @param isStartEvent indicator to identify whether this trigger will be * triggering a start or an end command. * * @throws SchedulerException if there is an internal Scheduler error. */ protected boolean createTriggerAndSchedule(JobDetail job, VEvent event, String modifiedByEvent, boolean isStartEvent) { boolean triggersCreated = false; if (job == null) { logger.debug("job is null -> no triggers are created"); return false; } String jobIdentity = event.getUid() + (isStartEvent ? "_start" : "_end"); long dateValue = isStartEvent ? event.getStartDate().getDate().getTime() : event.getEndDate().getDate().getTime(); /* TODO: TEE: do only create a new trigger when the start/endtime * lies in the future. This exclusion is necessary because the SimpleTrigger * triggers a job even if the startTime lies in the past. If somebody * knows the way to let quartz ignore such triggers this exclusion * can be omitted. */ if (dateValue >= DateTime.now().toDate().getTime()) { Trigger trigger; if (StringUtils.isBlank(modifiedByEvent)) { trigger = newTrigger().forJob(job) .withIdentity(jobIdentity + "_" + dateValue + "_trigger", CALDAV_SCHEDULER_GROUP) .startAt(new Date(dateValue)).build(); } else { trigger = newTrigger().forJob(job) .withIdentity(jobIdentity + "_" + dateValue + "_trigger", CALDAV_SCHEDULER_GROUP) .startAt(new Date(dateValue)).modifiedByCalendar(modifiedByEvent).build(); } try { scheduler.scheduleJob(job, trigger); triggersCreated = true; } catch (SchedulerException se) { logger.warn("scheduling Trigger '" + trigger + "' throws an exception.", se); } } return triggersCreated; } /** * Creates a detailed description of a <code>job</code> for logging purpose. * * @param job the job to create a detailed description for * @return a detailed description of the new <code>job</code> */ private String createJobInfo(VEvent event, JobDetail job) { if (job == null) { return "SchedulerJob [null]"; } StringBuffer sb = new StringBuffer(); sb.append("SchedulerJob [jobKey=").append(job.getKey().getName()); sb.append(", jobGroup=").append(job.getKey().getGroup()); try { List<? extends Trigger> triggers = scheduler.getTriggersOfJob(job.getKey()); sb.append(", ").append(triggers.size()).append(" triggers=["); int maxTriggerLogs = 24; for (int triggerIndex = 0; triggerIndex < triggers.size() && triggerIndex < maxTriggerLogs; triggerIndex++) { Trigger trigger = triggers.get(triggerIndex); sb.append(trigger.getStartTime()); if (triggerIndex < triggers.size() - 1 && triggerIndex < maxTriggerLogs - 1) { sb.append(", "); } } if (triggers.size() >= maxTriggerLogs) { sb.append(" and ").append(triggers.size() - maxTriggerLogs).append(" more ..."); } if (triggers.size() == 0) { sb.append("there are no triggers - probably the event lies in the past"); } } catch (SchedulerException e) { } /* * sb.append("], content=").append(event.getPlainTextContent()); */ return sb.toString(); } /** * Holds the parsed content of a GCal event * * @author Thomas.Eichstaedt-Engelen */ class CalendarEventContent { String startCommands = ""; String endCommands = ""; String modifiedByEvent = ""; } @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { if (config != null) { String usernameString = (String) config.get("username"); username = usernameString; if (StringUtils.isBlank(username)) { throw new ConfigurationException("caldav:username", "username must not be blank - please configure an aproppriate username in openhab.cfg"); } logger.trace("username: {}", username); String passwordString = (String) config.get("password"); password = passwordString; if (StringUtils.isBlank(password)) { throw new ConfigurationException("caldav:password", "password must not be blank - please configure an aproppriate password in openhab.cfg"); } logger.trace("password: {}", password); String hostString = (String) config.get("host"); host = hostString; if (StringUtils.isBlank(host)) { throw new ConfigurationException("caldav:host", "host must not be blank - please configure an aproppriate host in openhab.cfg"); } logger.trace("host: {}", host); String tlsString = (String) config.get("tls"); if (StringUtils.isNotBlank(tlsString)) { try { tls = Boolean.parseBoolean(tlsString); } catch (IllegalArgumentException iae) { logger.warn("couldn't parse caldav:tls '{}' to a boolean"); } } else { tls = true; } logger.trace("tls: {}", tls); String strictTlsString = (String) config.get("strict-tls"); if (StringUtils.isNotBlank(strictTlsString)) { try { strictTls = Boolean.parseBoolean(strictTlsString); } catch (IllegalArgumentException iae) { logger.warn("couldn't parse caldav:strict-tls '{}' to a boolean"); } } else { strictTls = true; } logger.trace("strictTls: {}", strictTls); if (!tls) { logger.warn( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); logger.warn( "!! You have disabled tls/ssl for CalDav-EventDownloader. Calendar data is exchanged unencrypted. !!"); logger.warn( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } if (!strictTls && tls) { logger.warn( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); logger.warn( "!! You have disabled strict certificate checking by setting strict-tls to false. !!"); logger.warn( "!! Actually all checking for certificates in CalDav-EventDownloader is disabled now !!"); logger.warn( "!! - which means that there is no real security - as you accept any certificate, !!"); logger.warn( "!! even those which might be injected for Man-In The Middle-Attacks - try to !!"); logger.warn( "!! Register your certificate to your java certificate store and set strict-tls to !!"); logger.warn( "!! true. Disable the tls checking is just meant for debugging purposes. !!"); logger.warn( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } String portString = (String) config.get("port"); if (StringUtils.isNotBlank(portString)) { try { port = Integer.valueOf(portString); } catch (IllegalArgumentException iae) { logger.warn("couldn't parse caldav:port '{}' to an integer"); } } else { if (tls) { port = 443; } else { port = 80; } } logger.trace("port: {}", port); String urlString = (String) config.get("url"); url = urlString; if (StringUtils.isBlank(url)) { throw new ConfigurationException("caldav:url", "url must not be blank - please configure an aproppriate url in openhab.cfg"); } logger.trace("url: {}", url); // filter = (String) config.get("filter"); String refreshString = (String) config.get("refresh"); if (StringUtils.isNotBlank(refreshString)) { refreshInterval = Integer.parseInt(refreshString); refreshInterval *= 1000; } logger.trace("refreshInterval: {}ms", refreshInterval); setProperlyConfigured(true); logger.debug("CalDav event downloader successfuly configured"); } } }