org.jasig.schedassist.impl.caldav.CaldavCalendarDataDaoImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.schedassist.impl.caldav.CaldavCalendarDataDaoImpl.java

Source

/**
 * 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);
        }
    }
}