/**
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.schedassist.impl.caldav;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.fortuna.ical4j.data.ParserException;
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.Parameter;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.parameter.PartStat;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.Status;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.Version;
import net.fortuna.ical4j.util.Calendars;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthScope;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.jasig.schedassist.ConflictExistsException;
import org.jasig.schedassist.ICalendarDataDao;
import org.jasig.schedassist.NullAffiliationSourceImpl;
import org.jasig.schedassist.SchedulingException;
import org.jasig.schedassist.impl.caldav.xml.ReportResponseHandlerImpl;
import org.jasig.schedassist.impl.events.AutomaticAppointmentCancellationEvent;
import org.jasig.schedassist.impl.events.AutomaticAppointmentCancellationEvent.Reason;
import org.jasig.schedassist.impl.events.AutomaticAttendeeRemovalEvent;
import org.jasig.schedassist.model.AppointmentRole;
import org.jasig.schedassist.model.AvailabilityReflection;
import org.jasig.schedassist.model.AvailableBlock;
import org.jasig.schedassist.model.AvailableSchedule;
import org.jasig.schedassist.model.AvailableVersion;
import org.jasig.schedassist.model.CommonDateOperations;
import org.jasig.schedassist.model.DefaultEventUtilsImpl;
import org.jasig.schedassist.model.ICalendarAccount;
import org.jasig.schedassist.model.IEventUtils;
import org.jasig.schedassist.model.IScheduleOwner;
import org.jasig.schedassist.model.IScheduleVisitor;
import org.jasig.schedassist.model.SchedulingAssistantAppointment;
import org.jasig.schedassist.model.VisitorLimit;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
/**
* Implementation of {@link ICalendarDataDao} for CalDAV-capable calendar servers.
*
* Requires the following be provided (via setter injection):
* <ul>
* <li>{@link HttpClient} instance.</li>
* <li>{@link HttpCredentialsProvider} for authentication.</li>
* <li>{@link CaldavDialect} instance.</li>
* <li>A String containing the value of the <i>caldav.cancelUpdatesVisitorCalendar property</i> (false by default).</li>
* </ul>
*
* The cancelUpdatesVisitorCalendar property can be useful for CalDAV servers that do not delete appointments in an attendee's calendar
* when the organizer deletes the event. Oracle Communications Suite Calendar server for example will not delete the event from the visitor's
* calendar when the event is removed from the owner's calendar; the event remains with it's STATUS property set to CANCELLED. Setting the
* <i>caldav.cancelUpdatesVisitorCalendar property</i> to true will add behavior to {@link #cancelAppointment(IScheduleVisitor, IScheduleOwner, VEvent)}
* and {@link #leaveAppointment(IScheduleVisitor, IScheduleOwner, VEvent)} remove the CANCELLED entries from the visitor's calendar.
*
* This instance constructs a {@link DefaultCaldavEventUtilsImpl} instance with a {@link NullAffiliationSourceImpl}; if you need to override the {@link IEventUtils}
* instance, a setter is provided ({@link #setEventUtils(IEventUtils)}).
*
* Lastly this instance constructs a {@link NoopHttpMethodInterceptorImpl} instance; if you need to
* override the {@link HttpMethodInterceptor} a setter is provided ({@link #setMethodInterceptor(HttpMethodInterceptor)}).
*
* @author Nicholas Blair, nblair@doit.wisc.edu
* @version $Id: CaldavCalendarDataDaoImpl.java 50 2011-05-05 21:07:25Z nblair $
*/
@Service("caldavCalendarDataDao")
public class CaldavCalendarDataDaoImpl implements ICalendarDataDao, InitializingBean {
static final Header IF_NONE_MATCH_HEADER = new BasicHeader("If-None-Match", "*");
static final Header ICALENDAR_CONTENT_TYPE_HEADER = new BasicHeader("Content-Type", "text/calendar");
static final String IF_MATCH_HEADER = "If-Match";
private static final Header DEPTH_HEADER = new BasicHeader("Depth", "1");
protected final Log log = LogFactory.getLog(this.getClass());
private HttpClient httpClient;
private CredentialsProviderFactory credentialsProviderFactory;
private HttpHost httpHost;
private AuthScope caldavAdminAuthScope;
private IEventUtils eventUtils = new CaldavEventUtilsImpl(new NullAffiliationSourceImpl());
private CaldavDialect caldavDialect;
private HttpMethodInterceptor methodInterceptor = new NoopHttpMethodInterceptorImpl();
private boolean cancelUpdatesVisitorCalendar = false;
private boolean reflectionEnabled = false;
private boolean preemptiveAuthenticationEnabled = false;
private boolean getCalendarPerformsPurgeDeclinedAttendees = true;
private AuthScheme preemptiveAuthenticationScheme;
private ApplicationEventPublisher applicationEventPublisher;
/**
* @param httpClient the httpClient to set
*/
@Autowired
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* @return the httpClient
*/
public HttpClient getHttpClient() {
return httpClient;
}
/**
* @return the credentialsProviderFactory
*/
public CredentialsProviderFactory getCredentialsProviderFactory() {
return credentialsProviderFactory;
}
/**
* @param credentialsProviderFactory the credentialsProviderFactory to set
*/
@Autowired
public void setCredentialsProviderFactory(
CredentialsProviderFactory credentialsProviderFactory) {
this.credentialsProviderFactory = credentialsProviderFactory;
}
/**
*
* @return
*/
public HttpHost getHttpHost() {
return httpHost;
}
/**
*
* @param httpHost
*/
@Autowired
public void setHttpHost(HttpHost httpHost) {
this.httpHost = httpHost;
}
/**
* @param caldavAdminAuthScope the caldavAdminAuthScope to set
*/
@Autowired
public void setCaldavAdminAuthScope(AuthScope caldavAdminAuthScope) {
this.caldavAdminAuthScope = caldavAdminAuthScope;
}
/**
* @return the caldavAdminAuthScope
*/
public AuthScope getCaldavAdminAuthScope() {
return caldavAdminAuthScope;
}
/**
* @param eventUtils the eventUtils to set
*/
@Autowired(required=false)
public void setEventUtils(IEventUtils eventUtils) {
this.eventUtils = eventUtils;
}
/**
* @param caldavDialect the caldavDialect to set
*/
@Autowired
public void setCaldavDialect(CaldavDialect caldavDialect) {
this.caldavDialect = caldavDialect;
}
/**
* @param methodInterceptor the methodInterceptor to set
*/
@Autowired(required=false)
public void setMethodInterceptor(HttpMethodInterceptor methodInterceptor) {
this.methodInterceptor = methodInterceptor;
}
/**
* @return the methodInterceptor
*/
public HttpMethodInterceptor getMethodInterceptor() {
return methodInterceptor;
}
/**
* @param applicationEventPublisher the applicationEventPublisher to set
*/
@Autowired
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* @param cancelUpdatesVisitorCalendar the cancelUpdatesVisitorCalendar to set
*/
@Value("${caldav.cancelUpdatesVisitorCalendar:false}")
public void setCancelUpdatesVisitorCalendar(String cancelUpdatesVisitorCalendar) {
this.cancelUpdatesVisitorCalendar = Boolean.parseBoolean(cancelUpdatesVisitorCalendar);
}
/**
* @param reflectionEnabled the reflectionEnabled to set
*/
@Value("${caldav.reflectionEnabled:false}")
public void setReflectionEnabled(boolean reflectionEnabled) {
this.reflectionEnabled = reflectionEnabled;
}
/**
* @return the cancelUpdatesVisitorCalendar
*/
public boolean isCancelUpdatesVisitorCalendar() {
return cancelUpdatesVisitorCalendar;
}
/**
* @param cancelUpdatesVisitorCalendar the cancelUpdatesVisitorCalendar to set
*/
public void setCancelUpdatesVisitorCalendar(boolean cancelUpdatesVisitorCalendar) {
this.cancelUpdatesVisitorCalendar = cancelUpdatesVisitorCalendar;
}
/**
* @return the eventUtils
*/
public IEventUtils getEventUtils() {
return eventUtils;
}
/**
* @return the caldavDialect
*/
public CaldavDialect getCaldavDialect() {
return caldavDialect;
}
/**
* @return the reflectionEnabled
*/
public boolean isReflectionEnabled() {
return reflectionEnabled;
}
/**
* @return the preemptiveAuthenticationEnabled
*/
public boolean isPreemptiveAuthenticationEnabled() {
return preemptiveAuthenticationEnabled;
}
/**
* @param preemptiveAuthenticationEnabled the preemptiveAuthenticationEnabled to set
*/
@Value("${caldav.preemptiveAuthenticationEnabled:false}")
public void setPreemptiveAuthenticationEnabled(
boolean preemptiveAuthenticationEnabled) {
this.preemptiveAuthenticationEnabled = preemptiveAuthenticationEnabled;
}
/**
* @return the getCalendarPerformsPurgeDeclinedAttendees
*/
public boolean isGetCalendarPerformsPurgeDeclinedAttendees() {
return getCalendarPerformsPurgeDeclinedAttendees;
}
/**
* @param getCalendarPerformsPurgeDeclinedAttendees the getCalendarPerformsPurgeDeclinedAttendees to set
*/
@Value("${caldav.getCalendarPerformsPurgeDeclinedAttendees:true}")
public void setGetCalendarPerformsPurgeDeclinedAttendees(
boolean getCalendarPerformsPurgeDeclinedAttendees) {
this.getCalendarPerformsPurgeDeclinedAttendees = getCalendarPerformsPurgeDeclinedAttendees;
}
/**
*
* @param scheme
* @return
*/
protected AuthScheme identifyScheme(String scheme) {
if(new BasicScheme().getSchemeName().equalsIgnoreCase(scheme)) {
return new BasicScheme();
} else if (new DigestScheme().getSchemeName().equalsIgnoreCase(scheme)) {
return new DigestScheme();
} else {
throw new IllegalArgumentException("cannot determine AuthScheme implementation from " + scheme);
}
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
if(isPreemptiveAuthenticationEnabled()) {
// addRequestInterceptor method not visible on the HttpClient interface
((AbstractHttpClient) this.httpClient).addRequestInterceptor(new PreemptiveAuthInterceptor(caldavAdminAuthScope), 0);
this.preemptiveAuthenticationScheme = identifyScheme(caldavAdminAuthScope.getScheme());
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#getCalendar(org.jasig.schedassist.model.ICalendarAccount, java.util.Date, java.util.Date)
*/
@Override
public Calendar getCalendar(ICalendarAccount calendarAccount,
Date startDate, Date endDate) {
List<CalendarWithURI> calendars = getCalendarsInternal(calendarAccount, startDate, endDate);
Calendar result = consolidate(calendars);
return result;
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#getExistingAppointment(org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.AvailableBlock)
*/
@Override
public VEvent getExistingAppointment(IScheduleOwner owner,
AvailableBlock block) {
CalendarWithURI calendarWithUri = getExistingAppointmentInternal(owner, block.getStartTime(), block.getEndTime());
if(null != calendarWithUri) {
ComponentList componentList = calendarWithUri.getCalendar().getComponents(VEvent.VEVENT);
return (VEvent) componentList.get(0);
} else {
return null;
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#createAppointment(org.jasig.schedassist.model.IScheduleVisitor, org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.AvailableBlock, java.lang.String)
*/
@Override
public VEvent createAppointment(IScheduleVisitor visitor,
IScheduleOwner owner, AvailableBlock block, String eventDescription) {
VEvent event = this.eventUtils.constructAvailableAppointment(
block,
owner,
visitor,
eventDescription);
try {
int statusCode = putNewEvent(owner.getCalendarAccount(), event);
if(log.isDebugEnabled()) {
log.debug("createAppointment status code: " + statusCode);
}
if(statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED) {
return event;
} else {
throw new CaldavDataAccessException("createAppointment for " + visitor + ", " + owner + ", " + block + " failed with unexpected status code: " + statusCode);
}
} catch (HttpException e) {
log.error("an HttpException occurred in createAppointment for " + owner + ", " + visitor + ", " + block);
throw new CaldavDataAccessException(e);
} catch (IOException e) {
log.error("an IOException occurred in createAppointment for " + owner + ", " + visitor + ", " + block);
throw new CaldavDataAccessException(e);
}
}
/*
* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#cancelAppointment(org.jasig.schedassist.model.IScheduleVisitor, org.jasig.schedassist.model.IScheduleOwner, net.fortuna.ical4j.model.component.VEvent)
*/
@Override
public void cancelAppointment(IScheduleVisitor visitor, IScheduleOwner owner, VEvent appointment) {
Date startTime = appointment.getStartDate().getDate();
Date endTime = appointment.getEndDate(true).getDate();
// first locate event/calendar in owner's account
CalendarWithURI calendarWithURI = getExistingAppointmentInternal(owner, startTime, endTime);
if(null != calendarWithURI) {
VEvent event = extractSchedulingAssistantAppointment(calendarWithURI);
Uid eventUid = event.getUid();
int status = deleteCalendar(calendarWithURI, owner.getCalendarAccount());
if(log.isDebugEnabled()) {
log.debug("cancelAppointment status code " + status + " for " + owner + ", " + eventUid);
}
if(cancelUpdatesVisitorCalendar) {
CalendarWithURI visitorCalendarWithURI = getExistingAppointmentInternalForVisitor(visitor, startTime, endTime, eventUid);
if(visitorCalendarWithURI != null) {
status = deleteCalendar(visitorCalendarWithURI, visitor.getCalendarAccount());
if(log.isDebugEnabled()) {
log.debug("cancelAppointment status code " + status + " for " + visitor + ", " + eventUid);
}
} else {
log.warn("cancelAppointment unable to locate event in schedule for visitor " + visitor + " with uid " + eventUid);
}
}
} else {
log.warn("cannot cancelAppointment for " + owner + ", no matching appointment found (" + appointment + ")");
}
}
/**
* Construct an {@link HttpContext} with a {@link CredentialsProvider} appropriate
* for the {@link ICalendarAccount} argument.
* Returned value is intended for use with {@link HttpClient#execute(HttpHost, HttpRequest, HttpContext)}.
*
* @param calendarAccount
* @return an appropriate {@link HttpContext} for the {@link ICalendarAccount}.
*/
protected HttpContext constructHttpContext(ICalendarAccount calendarAccount) {
CredentialsProvider credentialsProvider = this.credentialsProviderFactory.getCredentialsProvider(calendarAccount);
HttpContext context = new BasicHttpContext();
if(isPreemptiveAuthenticationEnabled()) {
if(preemptiveAuthenticationScheme == null) {
throw new IllegalStateException("preemptiveAuthentication is enabled, but the preemptiveAuthenticationScheme is null. Was afterPropertiesSet invoked?");
}
context.setAttribute(PreemptiveAuthInterceptor.PREEMPTIVE_AUTH, preemptiveAuthenticationScheme);
}
context.setAttribute(ClientContext.CREDS_PROVIDER, credentialsProvider);
return context;
}
/**
*
* @param calendarWithURI
* @param calendarAccount
* @return
*/
protected int deleteCalendar(CalendarWithURI calendarWithURI, ICalendarAccount calendarAccount) {
URI uri = this.caldavDialect.resolveCalendarURI(calendarWithURI);
HttpDelete method = new HttpDelete(uri.toString());
if(log.isDebugEnabled()) {
log.debug("deleteCalendar executing " + methodToString(method) + " for " + calendarAccount);
}
HttpRequest toExecute = methodInterceptor.doWithMethod(method, calendarAccount);
final HttpContext context = constructHttpContext(calendarAccount);
HttpEntity entity = null;
try {
HttpResponse response = this.httpClient.execute(httpHost, toExecute, context);
entity = response.getEntity();
int statusCode = response.getStatusLine().getStatusCode();
log.debug("deleteCalendar status code: " + statusCode);
if(statusCode == HttpStatus.SC_NO_CONTENT) {
return statusCode;
} else {
throw new CaldavDataAccessException("deleteCalendar for " + calendarAccount + ", " + calendarWithURI +" failed with unexpected status code: " + statusCode);
}
} catch (IOException e) {
log.error("an IOException occurred in deleteCalendar for " + calendarAccount + ", " + calendarWithURI);
throw new CaldavDataAccessException(e);
} finally {
quietlyConsume(entity);
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#joinAppointment(org.jasig.schedassist.model.IScheduleVisitor, org.jasig.schedassist.model.IScheduleOwner, net.fortuna.ical4j.model.component.VEvent)
*/
@Override
public VEvent joinAppointment(IScheduleVisitor visitor,
IScheduleOwner owner, VEvent appointment)
throws SchedulingException {
Date startTime = appointment.getStartDate().getDate();
Date endTime = appointment.getEndDate(true).getDate();
CalendarWithURI calendarWithURI = getExistingAppointmentInternal(owner, startTime, endTime);
if(null != calendarWithURI) {
VEvent event = extractSchedulingAssistantAppointment(calendarWithURI);
Attendee attendee = this.eventUtils.constructSchedulingAssistantAttendee(visitor.getCalendarAccount(), AppointmentRole.VISITOR);
event.getProperties().add(attendee);
try {
int statusCode = putExistingEvent(owner.getCalendarAccount(), event, calendarWithURI.getEtag());
log.debug("joinAppointment status code: " + statusCode);
if(statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED || statusCode == HttpStatus.SC_NO_CONTENT) {
return event;
} else if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
// event changed in the interim, fail fast
throw new SchedulingException("joinAppointment failed for " + visitor + " and " + owner + ", appointment was altered");
} else {
throw new CaldavDataAccessException("joinAppointment for " + visitor + ", " + owner + ", " + startTime + " failed with unexpected status code: " + statusCode);
}
} catch (IOException e) {
log.error("an IOException occurred in joinAppointment for " + owner + ", " + visitor + ", " + startTime);
throw new CaldavDataAccessException(e);
}
} else {
log.warn("cannot joinAppointment for " + owner + ", no matching appointment found (" + appointment + ")");
throw new SchedulingException("joinAppointment failed for " + visitor + " and " + owner + ", no matching appointment found");
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#leaveAppointment(org.jasig.schedassist.model.IScheduleVisitor, org.jasig.schedassist.model.IScheduleOwner, net.fortuna.ical4j.model.component.VEvent)
*/
@Override
public VEvent leaveAppointment(IScheduleVisitor visitor,
IScheduleOwner owner, VEvent appointment)
throws SchedulingException {
Date startTime = appointment.getStartDate().getDate();
Date endTime = appointment.getEndDate(true).getDate();
CalendarWithURI calendarWithURI = getExistingAppointmentInternal(owner, startTime, endTime);
if(null != calendarWithURI) {
VEvent event = extractSchedulingAssistantAppointment(calendarWithURI);
Uid eventUid = event.getUid();
Property attendee = this.eventUtils.getAttendeeForUserFromEvent(event, visitor.getCalendarAccount());
event.getProperties().remove(attendee);
try {
int statusCode = putExistingEvent(owner.getCalendarAccount(), event, calendarWithURI.getEtag());
log.debug("leaveAppointment status code: " + statusCode);
if(statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED || statusCode == HttpStatus.SC_NO_CONTENT) {
log.debug("leaveAppointment owner calendar update successful");
} else if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
// event changed in the interim, fail fast
throw new SchedulingException("leaveAppointment failed for " + visitor + " and " + owner + ", appointment was altered");
} else {
throw new CaldavDataAccessException("leaveAppointment for " + visitor + ", " + owner + ", " + startTime + " failed with unexpected status code: " + statusCode);
}
} catch (IOException e) {
log.error("an IOException occurred in leaveAppointment for " + owner + ", " + visitor + ", " + startTime);
throw new CaldavDataAccessException(e);
}
if(cancelUpdatesVisitorCalendar) {
CalendarWithURI visitorCalendarWithURI = getExistingAppointmentInternalForVisitor(visitor, startTime, endTime, eventUid);
if(visitorCalendarWithURI != null) {
int status = deleteCalendar(visitorCalendarWithURI, visitor.getCalendarAccount());
if(log.isDebugEnabled()) {
log.debug("leaveAppointment status code " + status + " for " + visitor + ", " + eventUid);
}
} else {
log.warn("leaveAppointment unable to locate event in schedule for visitor " + visitor + " with uid " + eventUid);
}
}
return event;
} else {
log.warn("cannot leaveAppointment for " + owner + ", no matching appointment found (" + appointment + ")");
throw new SchedulingException("leaveAppointment failed for " + visitor + " and " + owner + ", no matching appointment found");
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#checkForConflicts(org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.AvailableBlock)
*/
@Override
public void checkForConflicts(IScheduleOwner owner, AvailableBlock block)
throws ConflictExistsException {
// use a start and end time slightly smaller than the block to avoid events that start/end on the edge of the block
Date start = DateUtils.addSeconds(block.getStartTime(), 1);
Date end = DateUtils.addSeconds(block.getEndTime(), -1);
List<CalendarWithURI> calendars = getCalendarsInternal(owner.getCalendarAccount(), start, end);
for(CalendarWithURI calendar: calendars) {
ComponentList events = calendar.getCalendar().getComponents(VEvent.VEVENT);
for(Object component : events) {
VEvent event = (VEvent) component;
if(this.eventUtils.willEventCauseConflict(owner.getCalendarAccount(), event)) {
if(log.isDebugEnabled()) {
log.debug("conflict detected for " + owner + " at block " + block + ", event: " + event);
}
throw new ConflictExistsException("an appointment already exists for " + block);
}
}
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#reflectAvailableSchedule(org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.AvailableSchedule)
*/
@Override
public void reflectAvailableSchedule(IScheduleOwner owner,
AvailableSchedule schedule) {
if(reflectionEnabled) {
if(schedule.isEmpty()) {
return;
}
Date startDate = CommonDateOperations.beginningOfDay(schedule.getScheduleStartTime());
Date endDate = CommonDateOperations.endOfDay(schedule.getScheduleEndTime());
purgeAvailableScheduleReflections(owner, startDate, endDate);
List<Calendar> calendars = this.eventUtils.convertScheduleForReflection(schedule);
for(Calendar calendar: calendars) {
Uid uid = this.eventUtils.extractUid(calendar);
if(uid != null) {
//put!
try {
int statusCode = putNewCalendar(owner.getCalendarAccount(), calendar, uid.getValue());
if(statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED || statusCode == HttpStatus.SC_NO_CONTENT) {
//success
} else {
throw new CaldavDataAccessException("reflectAvailableSchedule for " + owner + " failed with unexpected status code: " + statusCode);
}
} catch (HttpException e) {
log.error("an HttpException occurred in reflectAvailableSchedule for " + owner);
throw new CaldavDataAccessException(e);
} catch (IOException e) {
log.error("an IOException occurred in reflectAvailableSchedule for " + owner);
throw new CaldavDataAccessException(e);
}
} else {
log.warn("cannot store reflection for calendar with no UID: " + calendar);
}
}
} else {
log.debug("experimental feature 'Availability Schedule reflection' disabled by default");
}
}
/* (non-Javadoc)
* @see org.jasig.schedassist.ICalendarDataDao#purgeAvailableScheduleReflections(org.jasig.schedassist.model.IScheduleOwner, java.util.Date, java.util.Date)
*/
@Override
public void purgeAvailableScheduleReflections(IScheduleOwner owner,
Date startDate, Date endDate) {
if(reflectionEnabled) {
List<CalendarWithURI> calendars = peekAtAvailableScheduleReflections(owner, startDate, endDate);
for(CalendarWithURI calendar: calendars) {
// delete!
URI uri = this.caldavDialect.resolveCalendarURI(calendar);
HttpDelete method = new HttpDelete(uri.toString());
if(log.isDebugEnabled()) {
log.debug("purgeAvailableScheduleReflections executing " + methodToString(method) + " for " + owner + ", " + startDate + ", " + endDate);
}
final HttpContext context = constructHttpContext(owner.getCalendarAccount());
HttpRequest toExecute = methodInterceptor.doWithMethod(method,owner.getCalendarAccount());
HttpEntity entity = null;
try {
HttpResponse response = this.httpClient.execute(httpHost, toExecute, context);
entity = response.getEntity();
int statusCode = response.getStatusLine().getStatusCode();
log.debug("cancelAppointment status code: " + statusCode);
if(statusCode == HttpStatus.SC_NO_CONTENT) {
//success
} else {
throw new CaldavDataAccessException("purgeAvailableScheduleReflections for " + owner + ", " + startDate + ", " + endDate +" failed with unexpected status code: " + statusCode);
}
} catch (IOException e) {
log.error("an IOException occurred in purgeAvailableScheduleReflections for " + owner + ", " + startDate + ", " + endDate);
throw new CaldavDataAccessException(e);
} finally {
quietlyConsume(entity);
}
}
} else {
log.debug("experimental feature 'Availability Schedule reflection' disabled");
}
}
/**
*
* @param owner
* @param startDate
* @param endDate
* @return
*/
public List<CalendarWithURI> peekAtAvailableScheduleReflections(IScheduleOwner owner,
Date startDate, Date endDate){
if(reflectionEnabled) {
List<CalendarWithURI> calendars = getCalendarsInternal(owner.getCalendarAccount(), startDate, endDate);
List<CalendarWithURI> results = new ArrayList<CalendarWithURI>();
for(CalendarWithURI calendar: calendars) {
ComponentList events = calendar.getCalendar().getComponents(VEvent.VEVENT);
for(Object component : events) {
VEvent event = (VEvent) component;
if(event.getProperties().contains(AvailabilityReflection.TRUE)) {
results.add(calendar);
}
}
}
return results;
} else {
log.debug("experimental feature 'Availability Schedule reflection' disabled");
return Collections.emptyList();
}
}
/**
* This method is intended to generate a unique URI to use with the PUT method
* in {@link #createAppointment(IScheduleVisitor, IScheduleOwner, AvailableBlock, String)}.
*
* It is implemented by the following:
* <pre>
caldavDialect.calculateCalendarAccountHome(owner.getCalendarAccount) + "/sched-assist-" + randomAlphanumeric + ".ics"
* </pre>
* @param owner
* @return
*/
protected String generateEventUri(ICalendarAccount owner, VEvent event) {
Validate.notNull(event, "event argument cannot be null");
Validate.notNull(event.getUid(), "cannot generateEventUri for event with null UID");
String accountHome = this.caldavDialect.getCalendarAccountHome(owner);
StringBuilder eventUri = new StringBuilder(accountHome);
eventUri.append(event.getUid().getValue());
eventUri.append(".ics");
return eventUri.toString();
}
/**
*
* @param owner
* @param eventUid
* @return
*/
protected String generateEventUri(ICalendarAccount owner, String eventUid) {
String accountHome = this.caldavDialect.getCalendarAccountHome(owner);
StringBuilder eventUri = new StringBuilder(accountHome);
eventUri.append(eventUid);
eventUri.append(".ics");
return eventUri.toString();
}
/**
*
* @param calendarAccount
* @param startDate
* @param endDate
* @return
* @throws IOException
*/
protected List<CalendarWithURI> getCalendarsInternal(ICalendarAccount calendarAccount,
Date startDate, Date endDate) {
String accountUri = this.caldavDialect.getCalendarAccountHome(calendarAccount);
HttpEntity requestEntity = caldavDialect.generateGetCalendarRequestEntity(startDate, endDate);
ReportMethod method = new ReportMethod(accountUri);
method.setEntity(requestEntity);
//method.addHeader(CONTENT_LENGTH_HEADER, Long.toString(requestEntity.getContentLength()));
method.addHeader(DEPTH_HEADER);
if(log.isDebugEnabled()) {
log.debug("getCalendarsInternal executing " + methodToString(method) + " for " + calendarAccount + ", start " + startDate + ", end " + endDate);
}
HttpRequest toExecute = methodInterceptor.doWithMethod(method,calendarAccount);
final HttpContext context = constructHttpContext(calendarAccount);
HttpEntity entity = null;
try {
HttpResponse response = this.httpClient.execute(httpHost, toExecute, context);
entity = response.getEntity();
int statusCode = response.getStatusLine().getStatusCode();
log.debug("getCalendarsInternal status code: " + statusCode);
if(statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_MULTI_STATUS) {
InputStream content = entity.getContent();
ReportResponseHandlerImpl reportResponseHandler = new ReportResponseHandlerImpl();
List<CalendarWithURI> calendars = reportResponseHandler.extractCalendars(content);
if(isGetCalendarPerformsPurgeDeclinedAttendees()) {
List<CalendarWithURI> results = new ArrayList<CalendarWithURI>();
for(CalendarWithURI c: calendars) {
if(purgeDeclinedAttendees(c, calendarAccount) != null) {
results.add(c);
}
}
return results;
}
// purgeDeclinedAttendees disabled
return calendars;
} else {
throw new CaldavDataAccessException("unexpected status code: " + statusCode);
}
} catch (IOException e) {
log.error("an IOException occurred in getCalendarsInternal for " + calendarAccount + ", " + startDate + ", " + endDate);
throw new CaldavDataAccessException(e);
} finally {
quietlyConsume(entity);
}
}
/**
* Consolidate the {@link Calendar}s within the argument, returning 1.
*
* @see Calendars#merge(Calendar, Calendar)
* @param calendars
* @return never null
* @throws ParserException
*/
protected Calendar consolidate(List<CalendarWithURI> calendars) {
final int size = calendars.size();
if(size == 0) {
return new Calendar();
} else if(size == 1) {
return calendars.get(0).getCalendar();
} else if (size == 2) {
return merge(calendars.get(0).getCalendar(), calendars.get(1).getCalendar());
} else {
// create target by merging first 2
Calendar main = merge(calendars.get(0).getCalendar(), calendars.get(1).getCalendar());
// loop over the rest
List<CalendarWithURI> remaining = calendars.subList(2, calendars.size());
for(Iterator<CalendarWithURI> i = remaining.iterator(); i.hasNext();) {
CalendarWithURI left = i.next();
// if there aren't any more in the iterator, merge an empty calendar
Calendar right = new Calendar();
if(i.hasNext()) {
right = i.next().getCalendar();
}
merge(main, left.getCalendar(), right);
}
return main;
}
}
/**
* Merge the components from all calendars into one result.
*
* @param calendars
* @return
*/
protected Calendar merge(Calendar left, Calendar right) {
Calendar result = new Calendar();
result.getProperties().add(DefaultEventUtilsImpl.PROD_ID);
result.getProperties().add(Version.VERSION_2_0);
merge(result, left, right);
return result;
}
/**
* Mutative method.
* The first {@link Calendar} argument is altered by this method.
*
* @param target
* @param left
* @param right
*/
protected void merge(Calendar target, Calendar left, Calendar right) {
Map<String, VTimeZone> existingTimezones = new HashMap<String, VTimeZone>();
// first pass is through the target to id VTIMEZONEs already stored
for(Iterator<?> i = target.getComponents().iterator(); i.hasNext();) {
Component c = (Component) i.next();
if(VTimeZone.VTIMEZONE.equals(c.getName())) {
VTimeZone tz = (VTimeZone) c;
existingTimezones.put(tz.getTimeZoneId().getValue(), tz);
}
}
// 2nd: pass through left
for(Iterator<?> i = left.getComponents().iterator(); i.hasNext();) {
Component c = (Component) i.next();
final boolean componentIsTimezone = VTimeZone.VTIMEZONE.equals(c.getName());
if(componentIsTimezone && existingTimezones.containsKey(((VTimeZone) c).getTimeZoneId().getValue())) {
// don't add this timezone, we've already got a copy
} else {
target.getComponents().add(c);
if(componentIsTimezone) {
VTimeZone tz = (VTimeZone) c;
existingTimezones.put(tz.getTimeZoneId().getValue(), tz);
}
}
}
// 3rd: iterate over the right
for(Iterator<?> i = right.getComponents().iterator(); i.hasNext();) {
Component c = (Component) i.next();
if(VTimeZone.VTIMEZONE.equals(c.getName()) && existingTimezones.containsKey(((VTimeZone) c).getTimeZoneId().getValue())) {
// don't add this timezone, we've already got a copy
} else {
target.getComponents().add(c);
}
}
}
/**
* This method returns the {@link CalendarWithURI} containing a single {@link VEvent} that
* was created with the Scheduling Assistant with the specified {@link IScheduleOwner} as the owner
* and the specified start and end times.
*
* @param owner
* @param startTime
* @param endTime
* @return the matching Scheduling Assistant {@link VEvent}, or null if none for this {@link IScheduleOwner} at the specified times
*/
protected CalendarWithURI getExistingAppointmentInternal(IScheduleOwner owner,
Date startTime, Date endTime) {
final DateTime targetStartTime = new DateTime(startTime);
final DateTime targetEndTime = new DateTime(endTime);
List<CalendarWithURI> calendars = getCalendarsInternal(owner.getCalendarAccount(), startTime, endTime);
for(CalendarWithURI calendarWithUri : calendars) {
ComponentList componentList = calendarWithUri.getCalendar().getComponents(VEvent.VEVENT);
if(componentList.size() != 1) {
// scheduling assistant creates calendars with only a single event, short-circuit on calendars with > 1 events
continue;
}
for(Object o: componentList) {
VEvent event = (VEvent) o;
Date eventStart = event.getStartDate().getDate();
Date eventEnd = event.getEndDate(true).getDate();
Property schedAssistProperty = event.getProperty(SchedulingAssistantAppointment.AVAILABLE_APPOINTMENT);
if(!SchedulingAssistantAppointment.TRUE.equals(schedAssistProperty)) {
// immediately skip over non-scheduling assistant appointments
continue;
}
// check for version first
Property versionProperty = event.getProperty(AvailableVersion.AVAILABLE_VERSION);
if (AvailableVersion.AVAILABLE_VERSION_1_2.equals(versionProperty)) {
// event has to be (1) an available appointment
// with (2) owner recognized as appointment owner and
// (3) start and (4) end date have to match
if(this.eventUtils.isAttendingAsOwner(event, owner.getCalendarAccount()) &&
eventStart.equals(targetStartTime) &&
eventEnd.equals(targetEndTime)) {
if(log.isDebugEnabled()) {
log.debug("getExistingAppointmentInternal found " + event);
}
return calendarWithUri;
}
}
}
}
// not found
return null;
}
/**
* Special method used when cancelUpdatesVisitorCalendar is set to true.
* Returns the {@link CalendarWithURI} in the visitor's account for the event
* with the specified start, end and eventuid.
*
* @param owner
* @param startTime
* @param endTime
* @param eventUid
* @return the matching event, or null if not found.
*/
protected CalendarWithURI getExistingAppointmentInternalForVisitor(IScheduleVisitor visitor, Date startTime, Date endTime, Uid eventUid) {
final DateTime targetStartTime = new DateTime(startTime);
final DateTime targetEndTime = new DateTime(endTime);
if(eventUid == null) {
log.debug("cannot call getExistingAppointmentInternal with null eventUid, visitor: " + visitor);
return null;
}
List<CalendarWithURI> calendars = getCalendarsInternal(visitor.getCalendarAccount(), startTime, endTime);
for(CalendarWithURI calendarWithUri : calendars) {
ComponentList componentList = calendarWithUri.getCalendar().getComponents(VEvent.VEVENT);
if(componentList.size() != 1) {
// scheduling assistant creates calendars with only a single event, short-circuit on calendars with > 1 events
continue;
}
for(Object o: componentList) {
VEvent event = (VEvent) o;
Date eventStart = event.getStartDate().getDate();
Date eventEnd = event.getEndDate(true).getDate();
Uid uid = event.getUid();
if(uid != null && eventUid.equals(uid)
&& Status.VEVENT_CANCELLED.equals(event.getStatus())
&& eventStart.equals(targetStartTime) &&
eventEnd.equals(targetEndTime)) {
return calendarWithUri;
}
}
}
// not found
return null;
}
/**
* Store a new calendar using CalDAV PUT.
*
* @param eventOwner
* @param event
* @return
* @throws HttpException
* @throws IOException
*/
protected int putNewCalendar(ICalendarAccount eventOwner, Calendar calendar, String eventUid) throws HttpException, IOException {
String uri = generateEventUri(eventOwner, eventUid);
HttpPut method = constructPutMethod(uri, calendar);
method.addHeader(IF_NONE_MATCH_HEADER);
HttpRequest toExecute = this.methodInterceptor.doWithMethod(method, eventOwner);
if(log.isDebugEnabled()) {
log.debug("putNewCalendar executing " + methodToString(method) + " for " + eventOwner);
}
final HttpContext context = constructHttpContext(eventOwner);
HttpEntity entity = null;
try {
HttpResponse response = this.httpClient.execute(httpHost, toExecute, context);
entity = response.getEntity();
if(log.isDebugEnabled()) {
if(entity == null) {
log.debug("putNewCalendar response entity was null, statusline: " + response.getStatusLine());
} else {
InputStream content = entity.getContent();
log.debug("putNewCalendar response body: " + IOUtils.toString(content));
}
}
int statusCode = response.getStatusLine().getStatusCode();
return statusCode;
} finally {
EntityUtils.consume(entity);
}
}
/**
* Store a new event using CalDAV PUT.
*
* @param eventOwner
* @param event
* @return
* @throws HttpException
* @throws IOException
*/
protected int putNewEvent(ICalendarAccount eventOwner, VEvent event) throws HttpException, IOException {
String uri = generateEventUri(eventOwner, event);
HttpPut method = constructPutMethod(uri, event);
method.addHeader(IF_NONE_MATCH_HEADER);
HttpRequest toExecute = this.methodInterceptor.doWithMethod(method, eventOwner);
if(log.isDebugEnabled()) {
log.debug("putNewEvent executing " + methodToString(method) + " for " + eventOwner);
}
final HttpContext context = constructHttpContext(eventOwner);
HttpEntity entity = null;
try {
HttpResponse response = this.httpClient.execute(httpHost, toExecute, context);
entity = response.getEntity();
if(log.isDebugEnabled()) {
if(entity == null) {
log.debug("putNewEvent response entity was null, statusline: " + response.getStatusLine());
} else {
InputStream content = entity.getContent();
log.debug("putNewEvent response body: " + IOUtils.toString(content));
}
}
int statusCode = response.getStatusLine().getStatusCode();
return statusCode;
} finally {
EntityUtils.consume(entity);
}
}
/**
* Update an existing event using CalDAV PUT.
*
* @param eventOwner
* @param event
* @param etag
* @return
* @throws HttpException
* @throws IOException
*/
protected int putExistingEvent(ICalendarAccount eventOwner, VEvent event, String etag) throws IOException {
String uri = generateEventUri(eventOwner, event);
HttpPut method = constructPutMethod(uri, event);
method.addHeader(IF_MATCH_HEADER, etag);
HttpRequest toExecute = this.methodInterceptor.doWithMethod(method, eventOwner);
if(log.isDebugEnabled()) {
log.debug("putExistingEvent executing " + methodToString(method) + " for " + eventOwner);
}
final HttpContext context = constructHttpContext(eventOwner);
HttpEntity entity = null;
try {
HttpResponse response = this.httpClient.execute(httpHost, toExecute, context);
entity = response.getEntity();
if(log.isDebugEnabled()) {
log.debug("putExistingEvent response entity is null, response status line: " + response.getStatusLine());
}
int statusCode = response.getStatusLine().getStatusCode();
return statusCode;
} finally {
EntityUtils.consume(entity);
}
}
/**
* This method will inspect {@link IScheduleVisitor} {@link Attendee}s among the {@link SchedulingAssistantAppointment}s
* in the {@link Calendar} argument.
* If an {@link Attendee} on an {@link SchedulingAssistantAppointment} has {@link Partstat#DECLINED}, the appointment
* will be cancelled (if one on one or lone visitor on group appt) or the attendee will be removed (group appointment
* with multiple attending visitors).
*
* @param calendarWithURI
* @param session
* @param owner
* @return the calendar minus any events or attendees that have been removed.
* @throws SchedulingException
* @throws StatusException
*/
protected CalendarWithURI purgeDeclinedAttendees(CalendarWithURI calendarWithURI, ICalendarAccount owner) {
ComponentList componentList = calendarWithURI.getCalendar().getComponents(VEvent.VEVENT);
if(componentList.size() != 1) {
return calendarWithURI;
}
for(Object o: componentList) {
VEvent event = (VEvent) o;
if(event.getStartDate().getDate().before(new java.util.Date())) {
// short-circuit non events in the past
continue;
}
final boolean hasAvailableAppointmentProperty = SchedulingAssistantAppointment.TRUE.equals(event.getProperty(SchedulingAssistantAppointment.AVAILABLE_APPOINTMENT));
final boolean isAttendingAsOwner = this.eventUtils.isAttendingAsOwner(event, owner);
if(hasAvailableAppointmentProperty && isAttendingAsOwner) {
PropertyList attendeeList = this.eventUtils.getAttendeeListFromEvent(event);
Property visitorLimitProp = event.getProperty(VisitorLimit.VISITOR_LIMIT);
final int visitorLimit = Integer.parseInt(visitorLimitProp.getValue());
for(Object a : attendeeList) {
Property attendee = (Property) a;
if(PartStat.DECLINED.equals(attendee.getParameter(PartStat.PARTSTAT))) {
log.trace("found attendee that has DECLINED event: " + attendee);
Parameter appointmentRole = attendee.getParameter(AppointmentRole.APPOINTMENT_ROLE);
if(AppointmentRole.OWNER.equals(appointmentRole) ) {
// remove whole appointment
deleteCalendar(calendarWithURI, owner);
log.warn("purgeDeclinedAttendees successfully cancelled appointment due to owner decline " + event);
this.applicationEventPublisher.publishEvent(new AutomaticAppointmentCancellationEvent(event, owner, Reason.OWNER_DECLINED));
return null;
} else if (AppointmentRole.VISITOR.equals(appointmentRole)) {
int availableVisitorCount = this.eventUtils.getScheduleVisitorCount(event);
if(visitorLimit > 1 && availableVisitorCount > 1) {
// remove only the attendee (leave event)
event.getProperties().remove(attendee);
try {
int statusCode = putExistingEvent(owner, event, calendarWithURI.getEtag());
log.debug("purgeDeclinedAttendees leave status code: " + statusCode);
if(statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED || statusCode == HttpStatus.SC_NO_CONTENT) {
log.warn("purgeDeclinedAttendees successfully removed declined attendee from group appointment " + event);
this.applicationEventPublisher.publishEvent(new AutomaticAttendeeRemovalEvent(event, owner, attendee));
} else if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
// event changed in the interim, fail fast
//throw new SchedulingException("purgeDeclinedAttendees leave failed for " + attendee + " and " + owner + ", appointment was altered");
// leave appointment as is
log.warn("purgeDeclinedAttendees leave failed for " + attendee + " and " + owner + ", appointment was altered");
} else {
throw new CaldavDataAccessException("purgeDeclinedAttendees leave failed for " + attendee + ", " + owner + " failed with unexpected status code: " + statusCode);
}
} catch (IOException e) {
log.error("an IOException occurred in joinAppointment for " + owner + ", " + attendee);
throw new CaldavDataAccessException(e);
}
} else {
// either one on one appointment or group appointment with only 1 visitor
// remove whole appointment
deleteCalendar(calendarWithURI, owner);
log.warn("purgeDeclinedAttendees successfully cancelled appointment due to no remaining visitors " + event);
this.applicationEventPublisher.publishEvent(new AutomaticAppointmentCancellationEvent(event, owner, Reason.NO_REMAINING_VISITORS));
return null;
}
}
}
}
} else {
if(log.isTraceEnabled()) {
String eventUid = "not set";
if(event.getUid() != null) {
eventUid = event.getUid().getValue();
}
log.trace("event (UID=" + eventUid + ") not a candidate for purge, hasAvailableAppointmentProperty=" + hasAvailableAppointmentProperty + ", isAttendingAsOwner=" + isAttendingAsOwner);
}
}
}
return calendarWithURI;
}
/**
*
* @param uri
* @param event
* @return
*/
HttpPut constructPutMethod(String uri, VEvent event) {
HttpPut method = new HttpPut(uri);
method.addHeader(ICALENDAR_CONTENT_TYPE_HEADER);
HttpEntity requestEntity = caldavDialect.generatePutAppointmentRequestEntity(event);
method.setEntity(requestEntity);
return method;
}
/**
*
* @param uri
* @param event
* @return
*/
HttpPut constructPutMethod(String uri, Calendar calendar) {
HttpPut method = new HttpPut(uri);
method.addHeader(ICALENDAR_CONTENT_TYPE_HEADER);
HttpEntity requestEntity = caldavDialect.generatePutAppointmentRequestEntity(calendar);
method.setEntity(requestEntity);
return method;
}
/**
* Method intended to pull the single {@link VEvent} from a
* {@link CalendarWithURI} containing a scheduling assistant appointment.
*
* @param calendar
* @return
*/
VEvent extractSchedulingAssistantAppointment(CalendarWithURI calendar) {
ComponentList events = calendar.getCalendar().getComponents(VEvent.VEVENT);
Validate.isTrue(events.size() == 1, "expecting calendar with single event");
return (VEvent) events.get(0);
}
/**
* Basic toString for {@link HttpRequest} to output method name and path.
*
* @param method
* @return
*/
String methodToString(HttpRequest method) {
return method.getRequestLine().toString();
}
/**
*
* @param entity
*/
void quietlyConsume(HttpEntity entity) {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
log.info("caught IOException from EntityUtils#consume", e);
}
}
}