/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of the License at the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jasig.portlet.calendar.adapter;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.portlet.PortletRequest;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
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.VEvent;
import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.Duration;
import net.fortuna.ical4j.model.property.RRule;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.portlet.calendar.CalendarConfiguration;
import org.jasig.portlet.calendar.caching.DefaultCacheKeyGeneratorImpl;
import org.jasig.portlet.calendar.caching.ICacheKeyGenerator;
import org.jasig.portlet.calendar.credentials.DefaultCredentialsExtractorImpl;
import org.jasig.portlet.calendar.credentials.ICredentialsExtractor;
import org.jasig.portlet.calendar.url.DefaultUrlCreatorImpl;
import org.jasig.portlet.calendar.url.IUrlCreator;
import org.joda.time.Interval;
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;
/**
* Implementation of {@link ICalendarAdapter} that uses CalDAV for retrieving ical-based {@link
* CalendarEventSet}s.
*
* <p>Useful background articles: <a
* href="http://blogs.nologin.es/rickyepoderi/index.php?/archives/14-Introducing-CalDAV-Part-I.html"/>
* <a
* href="http://blogs.nologin.es/rickyepoderi/index.php?/archives/15-Introducing-CalDAV-Part-II.html"/>
*
* <p>URL format varies per provider (see links above). For Google an URL would be
* https://www.google.com/calendar/dav/CALENDERID/events where CALENDARID is obtained from the
* properties of the calendars, such as jameswennmacher@gmail.com for a user's personal calendar, or
* en.usa%23holiday%40group.v.calendar.google.com for the US Holidays calendar. Google uses Basic
* Authentication (or oAuth 2.0 though this is not implemented yet).
*
* @author Jen Bourey, jennifer.bourey@gmail.com
* @version $Header: CalDavCalendarAdapter.java Exp $
*/
public class CalDavCalendarAdapter extends AbstractCalendarAdapter implements ICalendarAdapter {
protected final Log log = LogFactory.getLog(this.getClass());
private Cache cache;
private IUrlCreator urlCreator = new DefaultUrlCreatorImpl();
private ICredentialsExtractor credentialsExtractor = new DefaultCredentialsExtractorImpl();
private ICacheKeyGenerator cacheKeyGenerator = new DefaultCacheKeyGeneratorImpl();
private String cacheKeyPrefix = "default";
public void setCache(Cache cache) {
this.cache = cache;
}
public void setUrlCreator(IUrlCreator urlCreator) {
this.urlCreator = urlCreator;
}
public void setCredentialsExtractor(ICredentialsExtractor credentialsExtractor) {
this.credentialsExtractor = credentialsExtractor;
}
public void setCacheKeyGenerator(ICacheKeyGenerator cacheKeyGenerator) {
this.cacheKeyGenerator = cacheKeyGenerator;
}
public void setCacheKeyPrefix(String cacheKeyPrefix) {
this.cacheKeyPrefix = cacheKeyPrefix;
}
public CalendarEventSet getEvents(
CalendarConfiguration calendarConfiguration, Interval interval, PortletRequest request)
throws CalendarException {
Set<VEvent> events = new HashSet<VEvent>();
String url = this.urlCreator.constructUrl(calendarConfiguration, interval, request);
if (log.isDebugEnabled()) {
log.debug("generated url: " + url);
}
// try to get the cached calendar
String key =
cacheKeyGenerator.getKey(
calendarConfiguration, interval, request, cacheKeyPrefix.concat(".").concat(url));
Element cachedElement = this.cache.get(key);
CalendarEventSet eventSet;
if (cachedElement == null) {
Credentials credentials = credentialsExtractor.getCredentials(request);
// retrieve calendars for the current user
net.fortuna.ical4j.model.Calendar calendar = retrieveCalendar(url, interval, credentials);
// extract events from the calendars
events.addAll(convertCalendarToEvents(calendar, interval));
log.debug("contentProcessor found " + events.size() + " events");
// save the calendar event set to the cache
eventSet = insertCalendarEventSetIntoCache(this.cache, key, events);
} else {
eventSet = (CalendarEventSet) cachedElement.getValue();
}
return eventSet;
}
// Have not gotten queries working so this method returns all events in
// the calendar.
protected final net.fortuna.ical4j.model.Calendar retrieveCalendar(
String url, Interval interval, Credentials credentials) {
try {
// construct a HostConfiguration from the server URL
URL hostUrl = new URL(url);
int port = hostUrl.getPort();
if (port == -1) {
port = hostUrl.getDefaultPort();
}
HttpClient httpClient = new HttpClient();
httpClient
.getHostConfiguration()
.setHost(hostUrl.getHost(), port, Protocol.getProtocol(hostUrl.getProtocol()));
if (log.isDebugEnabled()) {
log.debug("connecting to calDAV host " + httpClient.getHostConfiguration().getHost());
}
if (credentials != null) {
httpClient.getState().setCredentials(AuthScope.ANY, credentials);
httpClient.getParams().setAuthenticationPreemptive(true);
}
String calendarPath = hostUrl.getPath();
CalDAVCollection calDAVCollection =
new CalDAVCollection(
calendarPath,
httpClient.getHostConfiguration(),
new CalDAV4JMethodFactory(),
CalDAVConstants.PROC_ID_DEFAULT);
// log.debug(calDAVCollection.testConnection(httpClient));
if (log.isDebugEnabled()) {
log.debug(calDAVCollection.getCalendarCollectionRoot());
}
// Haven't gotten queries working
// GenerateQuery gq = new GenerateQuery();
// gq.setTimeRange(new net.fortuna.ical4j.model.Date(interval
// .getStart().toDate()), new net.fortuna.ical4j.model.Date(
// interval.getEnd().toDate()));
// CalendarQuery query = gq.generate();
// Haven't gotten queries working even for single calendar.
// Also I don't think Google supports viewing
// or querying multiple calendars at once.
// List<Calendar> cals = calDAVCollection.queryCalendars(client, query);
// return cals.get(0);
Calendar cal = calDAVCollection.getCalendar(httpClient, "");
if (log.isDebugEnabled()) {
log.debug(cal);
}
return cal;
} catch (CalDAV4JException e) {
log.error("CalDAV exception: ", e);
throw new CalendarException(e);
} catch (Exception e) {
log.error(e);
throw new CalendarException("Unknown exception while retrieving calendar", e);
}
}
protected final Set<VEvent> convertCalendarToEvents(
net.fortuna.ical4j.model.Calendar calendar, Interval interval) throws CalendarException {
Period period =
new Period(
new net.fortuna.ical4j.model.DateTime(interval.getStartMillis()),
new net.fortuna.ical4j.model.DateTime(interval.getEndMillis()));
Set<VEvent> events = new HashSet<VEvent>();
// if the calendar is null, return empty set
if (calendar == null) {
log.info("calendar empty, returning empty set");
return Collections.emptySet();
}
// retrieve the list of events for this calendar within the
// specified time period
for (Iterator<Component> i = calendar.getComponents().iterator(); i.hasNext(); ) {
Component component = i.next();
if (component.getName().equals("VEVENT")) {
VEvent event = (VEvent) component;
if (log.isTraceEnabled()) {
log.trace("processing event " + event.getSummary().getValue());
}
// calculate the recurrence set for this event
// for the specified time period
PeriodList periods = event.calculateRecurrenceSet(period);
// add each recurrence instance to the event list
for (Iterator<Period> iter = periods.iterator(); iter.hasNext(); ) {
Period eventper = iter.next();
PropertyList props = event.getProperties();
// create a new property list, setting the date
// information to this event period
PropertyList newprops = new PropertyList();
newprops.add(new DtStart(eventper.getStart()));
newprops.add(new DtEnd(eventper.getEnd()));
for (Iterator<Property> iter2 = props.iterator(); iter2.hasNext(); ) {
Property prop = iter2.next();
// only add non-date-related properties
if (!(prop instanceof DtStart)
&& !(prop instanceof DtEnd)
&& !(prop instanceof Duration)
&& !(prop instanceof RRule)) newprops.add(prop);
}
// create the new event from our property list
VEvent newevent = new VEvent(newprops);
events.add(newevent);
if (log.isTraceEnabled()) {
log.trace("added event " + newevent);
}
}
}
}
return events;
}
}