/*
* Copyright 2005 Open Source Applications Foundation
*
* Licensed 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.osaf.caldav4j;
import static org.osaf.caldav4j.util.ICalendarUtils.getMasterEvent;
import static org.osaf.caldav4j.util.ICalendarUtils.getUIDValue;
import static org.osaf.caldav4j.util.UrlUtils.stripHost;
import java.io.IOException;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;
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.Date;
import net.fortuna.ical4j.model.component.CalendarComponent;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.property.CalScale;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.Version;
import net.fortuna.ical4j.util.CompatibilityHints;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.webdav.lib.Ace;
import org.apache.webdav.lib.PropertyName;
import org.apache.webdav.lib.methods.AclMethod;
import org.apache.webdav.lib.methods.DepthSupport;
import org.osaf.caldav4j.exceptions.BadStatusException;
import org.osaf.caldav4j.exceptions.CalDAV4JException;
import org.osaf.caldav4j.exceptions.ResourceNotFoundException;
import org.osaf.caldav4j.exceptions.ResourceNotFoundException.IdentifierType;
import org.osaf.caldav4j.methods.CalDAV4JMethodFactory;
import org.osaf.caldav4j.methods.CalDAVReportMethod;
import org.osaf.caldav4j.methods.DelTicketMethod;
import org.osaf.caldav4j.methods.DeleteMethod;
import org.osaf.caldav4j.methods.GetMethod;
import org.osaf.caldav4j.methods.HttpClient;
import org.osaf.caldav4j.methods.MkCalendarMethod;
import org.osaf.caldav4j.methods.MkTicketMethod;
import org.osaf.caldav4j.methods.PropFindMethod;
import org.osaf.caldav4j.methods.PutMethod;
import org.osaf.caldav4j.model.request.CalendarData;
import org.osaf.caldav4j.model.request.CalendarMultiget;
import org.osaf.caldav4j.model.request.CalendarQuery;
import org.osaf.caldav4j.model.request.PropProperty;
import org.osaf.caldav4j.model.request.TicketRequest;
import org.osaf.caldav4j.model.response.CalDAVResponse;
import org.osaf.caldav4j.model.response.TicketDiscoveryProperty;
import org.osaf.caldav4j.model.response.TicketResponse;
import org.osaf.caldav4j.model.util.PropertyFactory;
import org.osaf.caldav4j.util.CaldavStatus;
import org.osaf.caldav4j.util.GenerateQuery;
import org.osaf.caldav4j.util.ICalendarUtils;
import org.osaf.caldav4j.util.MethodUtil;
import org.osaf.caldav4j.util.UrlUtils;
/**
* This class provides a high level API to a calendar collection on a CalDAV server.
*
* @author robipolli@gmail.com
*
* implements methods for
* - create
* - retrieve
* - update
* - delete
*
* calendars are retrieved in two ways
* - by path (with get methods)
* - by custom query (with search methods)
* no customized queries should be public in this class
*/
public class CalDAVCollection extends CalDAVCalendarCollectionBase{
private static final Log log = LogFactory.getLog(CalDAVCollection.class);
// configuration settings
public CalDAVCollection(){
}
/**
* Creates a new CalDAVCalendar collection with the specified paramters
*
* @param path The path to the collection
* @param hostConfiguration Host information for the CalDAV Server
* @param methodFactory methodFactory to obtail HTTP methods from
* @param prodId String identifying who creates the iCalendar objects
*/
public CalDAVCollection(String path,
HostConfiguration hostConfiguration,
CalDAV4JMethodFactory methodFactory, String prodId) {
setCalendarCollectionRoot(path);
this.hostConfiguration = hostConfiguration;
this.methodFactory = methodFactory;
this.prodId = prodId;
}
//Configuration Methods
/**
* Returns the icalendar object which contains the event with the specified
* UID.
*
* @param httpClient the httpClient which will make the request
* @param uid The uniqueID of the event to find
* @return the Calendar object containing the event with this UID
* @throws CalDAV4JException if there was a problem, or if the resource could
* not be found.
* @deprecated use a less-specialized query
*/
public Calendar getCalendarForEventUID(HttpClient httpClient, String uid)
throws CalDAV4JException, ResourceNotFoundException {
// implement it using a simplequery: here we don't need meta-data/tags
return getCalDAVResourceForEventUID(httpClient, uid).getCalendar();
}
/**
* Gets an icalendar object by GET
*
* @param httpClient the httpClient which will make the request
* @param icsRelativePath the path, relative to the collection path
* @return the Calendar object at the specified path
* @throws CalDAV4JException
*/
public Calendar getCalendar(HttpClient httpClient, String icsRelativePath)
throws CalDAV4JException{
return getCalDAVResource(httpClient, getAbsolutePath(icsRelativePath)).getCalendar();
}
/**
* Retrieve a single calendar by UID / COMPONENT using REPORT
* @param httpClient
* @param component
* @param uid
* @param recurrenceId
* @return The Calendar with the given UID. null if not found
* @throws CalDAV4JException
*/
public Calendar queryCalendar(HttpClient httpClient, String component, String uid, String recurrenceId) throws CalDAV4JException {
String filter = String.format("%s : UID==%s", component, uid);
if (recurrenceId != null) {
filter = String.format("%s, RECURRENCE-ID==%s", filter, recurrenceId);
}
GenerateQuery gq = new GenerateQuery(component, filter);
List<Calendar> cals = queryCalendars(httpClient, gq.generate());
switch (cals.size()) {
case 1:
return cals.get(0);
case 0:
return null;
default:
throw new CalDAV4JException("More than one calendar returned for uid "+uid);
}
}
/**
* Returns all Calendars which contain events which have instances who fall within
* the two dates. Note that recurring events are NOT expanded.
*
* @param httpClient the httpClient which will make the request
* @param beginDate the beginning of the date range. Must be a UTC date
* @param endDate the end of the date range. Must be a UTC date.
* @return a List of Calendars
* @throws CalDAV4JException if there was a problem
*
* @deprecated should be implemented by query
*/
public List<Calendar> getEventResources(HttpClient httpClient,
Date beginDate, Date endDate)
throws CalDAV4JException {
GenerateQuery gq = new GenerateQuery();
gq.setFilter("VEVENT");
gq.setTimeRange(beginDate, endDate);
return queryCalendars(httpClient, gq.generate());
}
/**
* Delete every component with the given UID. As UID is unique in the
* collection it should remove only one Calendar resource
*
* @param httpClient
* @param uid
* @throws CalDAV4JException
*
* TODO this method should be refined with recurrenceid
*/
public void delete(HttpClient httpClient, String component, String uid)
throws CalDAV4JException{
CalDAVResource resource = getCalDAVResourceByUID(httpClient, component, uid);
Calendar calendar = resource.getCalendar();
ComponentList eventList = calendar.getComponents().getComponents(component);
// get a list of components to remove
List<Component> componentsToRemove = new ArrayList<Component>();
boolean hasOtherEvents = false;
for (Object o : eventList){
CalendarComponent event = (CalendarComponent) o;
String curUID = ICalendarUtils.getUIDValue(event);
if (!uid.equals(curUID)){
hasOtherEvents = true;
} else {
componentsToRemove.add(event);
}
}
//
// remove from calendar the components with the given UID
// and PUT the calendar
//
if (hasOtherEvents){
if (componentsToRemove.size() == 0){
throw new ResourceNotFoundException(
ResourceNotFoundException.IdentifierType.UID, uid);
}
for (Component removeMe : componentsToRemove){
calendar.getComponents().remove(removeMe);
}
put(httpClient, calendar, stripHost(resource.getResourceMetadata().getHref()),
resource.getResourceMetadata().getETag());
return;
} else {
delete(httpClient, stripHost(resource.getResourceMetadata().getHref()));
}
}
/**
* Creates a calendar at the specified path
*
*/
public void createCalendar(HttpClient httpClient) throws CalDAV4JException{
MkCalendarMethod mkCalendarMethod = new MkCalendarMethod();
mkCalendarMethod.setPath(getCalendarCollectionRoot());
try {
httpClient.executeMethod(hostConfiguration, mkCalendarMethod);
int statusCode = mkCalendarMethod.getStatusCode();
if (statusCode != CaldavStatus.SC_CREATED){
MethodUtil.StatusToExceptions(mkCalendarMethod);
}
} catch (Exception e){
throw new CalDAV4JException("Trouble executing MKCalendar", e);
}
}
/**
* Adds a new Calendar with the given Component and VTimeZone to the collection.
*
* Tries to use the event UID followed by ".ics" as the name of the
* resource, otherwise will use the UID followed by a random number and
* ".ics"
*
* @param httpClient the httpClient which will make the request
* @param vevent The VEvent to put in the Calendar
*
* @param timezone The VTimeZone of the VEvent if it references one,
* otherwise null
* @throws CalDAV4JException
* @todo specify somewhere the kind of caldav error...
*/
public void add(HttpClient httpClient, CalendarComponent vevent, VTimeZone timezone)
throws CalDAV4JException {
Calendar calendar = new Calendar();
calendar.getProperties().add(new ProdId(prodId));
calendar.getProperties().add(Version.VERSION_2_0);
calendar.getProperties().add(CalScale.GREGORIAN);
if (timezone != null){
calendar.getComponents().add(timezone);
}
calendar.getComponents().add(vevent);
//
// retry 3 times while caldav server returns PRECONDITION_FAILED
//
boolean didIt = false;
for (int x = 0; x < 3 && !didIt; x++) {
String resourceName = null;
String uid = ICalendarUtils.getUIDValue(vevent);
// change UID at second attempt
if (x > 0) {
uid += "-"+random.nextInt();
ICalendarUtils.setUIDValue(calendar, uid);
}
resourceName = uid + ".ics";
PutMethod putMethod = createPutMethodForNewResource(resourceName,
calendar);
try {
httpClient.executeMethod(getHostConfiguration(), putMethod);
//fixed for nullpointerexception
// A caldav server should always return a valid ETAG for given event, but google doesn't
String etag = StringUtils.defaultString(UrlUtils.getHeaderPrettyValue(putMethod, HEADER_ETAG),"");
CalDAVResource calDAVResource = new CalDAVResource(calendar,
etag, getHref((putMethod.getPath())));
cache.putResource(calDAVResource);
} catch (Exception e) {
throw new CalDAV4JException("Trouble executing PUT", e);
}
int statusCode = putMethod.getStatusCode();
switch (statusCode) {
case CaldavStatus.SC_CREATED:
case CaldavStatus.SC_NO_CONTENT:
didIt = true;
break;
case CaldavStatus.SC_PRECONDITION_FAILED:
// event not added, retry
break;
default:
MethodUtil.StatusToExceptions(putMethod);
} // switch
}
}
/**
* adds a calendar object to caldav collection using UID.ics as file name
* @param httpClient
* @param c
* @throws CalDAV4JException
*/
public void add(HttpClient httpClient, Calendar c)
throws CalDAV4JException {
//
// retry 3 times while caldav server returns PRECONDITION_FAILED
//
boolean didIt = false;
for (int x = 0; x < 3 && !didIt; x++) {
String resourceName = null;
String uid = ICalendarUtils.getUIDValue(c);
// change UID at second attempt
if (x > 0) {
uid += "-"+random.nextInt();
ICalendarUtils.setUIDValue(c, uid);
}
// TODO move all these lines into ICalendarUtils
uid = ICalendarUtils.getUIDValue(c);
if (uid == null) {
uid = random.nextLong() + "-" + random.nextLong();
ICalendarUtils.setUIDValue(c, uid);
}
PutMethod putMethod = createPutMethodForNewResource(uid + ".ics", c);
try {
httpClient.executeMethod(getHostConfiguration(), putMethod);
// TODO see RFC to check fallback if no etags are passed in response
String etag = StringUtils.defaultString(UrlUtils.getHeaderPrettyValue(putMethod, HEADER_ETAG),"");
CalDAVResource calDAVResource = new CalDAVResource(c, etag, getHref((putMethod.getPath())));
cache.putResource(calDAVResource);
} catch (Exception e) {
throw new CalDAV4JException("Trouble executing PUT", e);
}
int statusCode = putMethod.getStatusCode();
switch (statusCode) {
case CaldavStatus.SC_CREATED:
case CaldavStatus.SC_NO_CONTENT:
didIt = true;
break;
default:
MethodUtil.StatusToExceptions(putMethod);
}
} //for
}
/**
* Updates the resource containing the VEvent with the same UID as the given
* VEvent with the given VEvent
*
* TODO: Deal with SEQUENCE
* TODO: Handle timezone!!! Right now ignoring the param...
*
* @param httpClient the httpClient which will make the request
* @param vevent the vevent to update
* @param timezone The VTimeZone of the VEvent if it references one,
* otherwise null
* @throws CalDAV4JException
*/
public void updateMasterEvent(HttpClient httpClient, VEvent vevent, VTimeZone timezone)
throws CalDAV4JException{
String uid = getUIDValue(vevent);
CalDAVResource resource = getCalDAVResourceByUID(httpClient, Component.VEVENT, uid);
Calendar calendar = resource.getCalendar();
//let's find the master event first!
VEvent originalVEvent = getMasterEvent(calendar, uid);
calendar.getComponents().remove(originalVEvent);
calendar.getComponents().add(vevent);
put(httpClient, calendar,
stripHost(resource.getResourceMetadata().getHref()),
resource.getResourceMetadata().getETag());
}
/**
* Creates a ticket for the specified resource and returns the ticket id.
*
* @param httpClient the httpClient which will make the request
* @param relativePath the path, relative to the collection path for
* which to grant the ticket on
* @param visits
* @param timeout
* @param read
* @param write
* @return The id of the created ticket
* @throws CalDAV4JException
* Is thrown if the execution of the MkTicketMethod fails
*/
public String createTicket(HttpClient httpClient, String relativePath,
Integer visits, Integer timeout, boolean read, boolean write)
throws CalDAV4JException {
TicketRequest ticketRequest = new TicketRequest();
ticketRequest.setVisits(visits);
ticketRequest.setTimeout(timeout);
ticketRequest.setRead(read);
ticketRequest.setWrite(write);
// Make the ticket
MkTicketMethod mkTicketMethod = methodFactory.createMkTicketMethod();
mkTicketMethod.setPath(getAbsolutePath(relativePath));
mkTicketMethod.setTicketRequest(ticketRequest);
try {
httpClient.executeMethod(hostConfiguration, mkTicketMethod);
int statusCode = mkTicketMethod.getStatusCode();
if (statusCode != CaldavStatus.SC_OK) {
throw new CalDAV4JException("Create Ticket Failed with Status: "
+ statusCode + " and body: \n"
+ mkTicketMethod.getResponseBodyAsString());
}
} catch (Exception e) {
throw new CalDAV4JException("Trouble executing MKTicket", e);
}
TicketResponse ticketResponse = null;
try {
ticketResponse = mkTicketMethod.getResponseBodyAsTicketResponse();
} catch (Exception e) {
throw new CalDAV4JException("Trouble handling MkTicket Response", e);
}
return ticketResponse.getID();
}
/**
* Deletes the specified ticket on the specified resource.
*
* @param httpClient the httpClient which will make the request
* @param relativePath the path, relative to the collection path for
* which to revoke the ticket
* @param ticketID the ticketID which to revoke
* @throws CalDAV4JException
* Is thrown if the execution of the DelTicketMethod fails
*/
public void deleteTicket(HttpClient httpClient, String relativePath, String ticketId)
throws CalDAV4JException {
DelTicketMethod delTicketMethod = methodFactory.createDelTicketMethod();
delTicketMethod.setPath(getAbsolutePath(relativePath));
delTicketMethod.setTicket(ticketId);
try {
httpClient.executeMethod(hostConfiguration, delTicketMethod);
int statusCode = delTicketMethod.getStatusCode();
if (statusCode != CaldavStatus.SC_NO_CONTENT) {
throw new CalDAV4JException(
"Delete Ticket Failed with Status: " + statusCode
+ " and body: \n"
+ delTicketMethod.getResponseBodyAsString());
}
} catch (Exception e) {
throw new CalDAV4JException("Trouble executing DelTicket", e);
}
}
/**
* Returns all the ticket ID's from all tickets the requesting user has
* permision to view on a resource.
*
* @param httpClient the httpClient which will make the request
* @param relativePath the path, relative to the collection path for which
* to get the tickets
* @return
* @throws CalDAV4JException
* @throws HttpException
* @throws IOException
*/
public List<String> getTicketsIDs(HttpClient httpClient, String relativePath)
throws CalDAV4JException, HttpException, IOException {
Vector<PropertyName> properties = new Vector<PropertyName>();
PropertyName ticketDiscoveryProperty = new PropertyName(CalDAVConstants.NS_XYTHOS,
CalDAVConstants.ELEM_TICKETDISCOVERY);
PropertyName ownerProperty = new PropertyName(CalDAVConstants.NS_DAV,
"owner");
properties.add(ticketDiscoveryProperty);
properties.add(ownerProperty);
PropFindMethod propFindMethod = methodFactory.createPropFindMethod();
propFindMethod.setDepth(0);
propFindMethod.setType(0);
propFindMethod.setPath(getAbsolutePath(relativePath));
propFindMethod.setPropertyNames(properties.elements());
httpClient.executeMethod(hostConfiguration, propFindMethod);
int statusCode = propFindMethod.getStatusCode();
if (statusCode != CaldavStatus.SC_MULTI_STATUS) {
throw new CalDAV4JException("PropFind Failed with Status: "
+ statusCode + " and body: \n"
+ propFindMethod.getResponseBodyAsString());
}
String href = getHref(getAbsolutePath(relativePath));
Enumeration responses = propFindMethod.getResponseProperties(href);
List<String> ticketIDList = new ArrayList<String>();
while (responses.hasMoreElements()) {
org.apache.webdav.lib.Property item = (org.apache.webdav.lib.Property) responses
.nextElement();
if (item.getLocalName()
.equals(CalDAVConstants.ELEM_TICKETDISCOVERY)) {
TicketDiscoveryProperty ticketDiscoveryProp = (TicketDiscoveryProperty) item;
ticketIDList.addAll(ticketDiscoveryProp.getTicketIDs());
}
}
return ticketIDList;
}
/**
* get a CalDAVResource by UID
* it tries
* - first by a REPORT
* - then by GET /path
* @param httpClient
* @param uid
* @return
* @throws Exception
* @deprecated this query is too specialized @see{getCalDAVResourceByUID()}
*/
private CalDAVResource getCalDAVResourceForEventUID(
HttpClient httpClient, String uid) throws CalDAV4JException {
return getCalDAVResourceByUID(httpClient, Component.VEVENT, uid);
}
/**
*
* it tries
* - first by a REPORT
* - then by GET /path checking that UID=filename
* TODO another strategy can be to
* - first by GET /path and check UID
* - else try by report
* as the first case is the most common, I avoid overload the server with REPORT
* @param httpClient
* @param component
* @param uid
* @return a Caldav resource containing the component type with the given uid
* @throws Exception
*/
protected CalDAVResource getCalDAVResourceByUID(
HttpClient httpClient, String component, String uid)
throws CalDAV4JException, ResourceNotFoundException {
//first check the cache!
String href = cache.getHrefForEventUID(uid);
CalDAVResource resource = null;
if (href != null) {
resource = getCalDAVResource(httpClient, stripHost(href));
if (resource != null) {
return resource;
}
} else {
// check if there's an event with the standard caldav url
// TODO this method retrieves a VTIMEZONE on google calendar, due to a google-bug.
// check current behaviour!!!
try {
resource = getCalDAVResource(httpClient, getAbsolutePath(uid+".ics") );
if (uid.equals(getUIDValue(ICalendarUtils.getFirstComponent(resource, component)))) {
return resource;
}
} catch (Exception e){
// resource not found: continue...
resource = null;
}
}
// then check by calendar query
GenerateQuery gq;
gq = new GenerateQuery(null, component + " : UID=="+uid );
List<CalDAVResource> cr;
cr = getCalDAVResources(httpClient, gq.generate());
try {
resource = cr.get(0);
if (uid.equals(getUIDValue(ICalendarUtils.getFirstComponent(resource, component)))) {
cache.putResource(resource);
return resource;
} else {
throw new Exception();
}
} catch (Exception e) {
throw new ResourceNotFoundException(
ResourceNotFoundException.IdentifierType.UID, uid);
}
}
/**
* GET the resource at the given path. Will check the cache first, and compare that to the
* latest etag obtained using a HEAD request.
*
* if calendar resource in cache is void, retrieve directly from server (avoid get etag only)
* @param httpClient
* @param path
* @return
* @throws CalDAV4JException
* FIXME testme
*/
protected CalDAVResource getCalDAVResource(HttpClient httpClient,
String path) throws CalDAV4JException {
CalDAVResource calDAVResource = cache.getResource(getHref(path));
if (calDAVResource == null || calDAVResource.getCalendar() == null) {
return getCalDAVResourceFromServer(httpClient, path);
} else {
String currentEtag = getETag(httpClient, path);
return getCalDAVResource(httpClient, path, currentEtag);
}
}
/**
* Gets the resource for the given href. Will check the cache first, and if a cached
* version exists that has the etag provided it will be returned. Otherwise, it goes
* to the server for the resource.
*
* @param httpClient
* @param path
* @param currentEtag
* @return
* @throws CalDAV4JException
*/
protected CalDAVResource getCalDAVResource(HttpClient httpClient,
String path, String currentEtag) throws CalDAV4JException {
//first try getting from the cache
CalDAVResource calDAVResource = cache.getResource(getHref(path));
//ok, so we got the resource...but has it been changed recently?
if (calDAVResource != null
&& calDAVResource.getCalendar() != null) { // FIXME calDAVResource's calendar should not be null!
String cachedEtag = calDAVResource.getResourceMetadata().getETag();
if (cachedEtag.equals(currentEtag)){
return calDAVResource;
}
}
//either the etag was old, or it wasn't in the cache so let's get it
//from the server
return getCalDAVResourceFromServer(httpClient, path);
}
/**
* Gets a CalDAVResource (not a mere timezone) from the server - in other words DOES NOT check the cache.
* Adds the new resource to the cache, replacing any pre-existing version.
* On Google Caldav Server, this method skips VTIMEZONE resources as they are used as tombstones
*
* @param httpClient
* @param path
* @return CalDAVResource
* @throws CalDAV4JException
*/
protected CalDAVResource getCalDAVResourceFromServer(HttpClient httpClient,
String path) throws CalDAV4JException {
CalDAVResource calDAVResource = null;
GetMethod getMethod = getMethodFactory().createGetMethod();
getMethod.setPath(path);
try {
httpClient.executeMethod(hostConfiguration, getMethod);
} catch (Exception e){
throw new CalDAV4JException("Problem executing get method",e);
}
if (getMethod.getStatusCode() != CaldavStatus.SC_OK){
MethodUtil.StatusToExceptions(getMethod);
throw new BadStatusException(getMethod);
}
String href = getHref(path);
String etag = getMethod.getResponseHeader(HEADER_ETAG).getValue();
Calendar calendar = null;
try {
// XXX relaxed parsing can cause problem
// with converted x-vcalendar so check deeply into that stuff
// probably we only need to manage line-folding..
// if it doesn't parse, try again disabling quick-parsing @see{CompatibilityHints}
try {
calendar = getMethod.getResponseBodyAsCalendar();
} catch (ParserException pe) {
if (! isTolerantParsing()) {
throw pe;
}
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, false);
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, false);
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true);
CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_VALIDATION, false);
calendar = getMethod.getResponseBodyAsCalendar();
}
} catch (Exception e) {
try {
log.error(getMethod.getResponseBodyAsString());
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
throw new CalDAV4JException("Malformed calendar resource returned at path: "
+ getMethod.getPath(), e);
}
// XXX if calendar is more than a single timezone (this kludge is needed for google calendar)
if (!isGoogleTombstone(calendar)) {
calDAVResource = new CalDAVResource();
calDAVResource.setCalendar(calendar);
calDAVResource.getResourceMetadata().setETag(etag);
calDAVResource.getResourceMetadata().setHref(href);
}
cache.putResource(calDAVResource);
return calDAVResource;
}
/**
* check if calendar is a tombstone. always false if skipGoogleTombstones is true
* @param calendar
* @return true if I object is a tombstone and tombstone-checking enabled
*/
private boolean isGoogleTombstone(Calendar calendar) {
/*
if (this.skipGoogleTombstones && (calendar != null )) {
// is it a tombstone?
if ( calendar.getComponents().size() ==1 &&
(calendar.getProductId().getValue().matches("Google Calendar")) &&
(calendar.getComponents().get(0) instanceof VTimeZone ) )
return true;
}
*/
return false;
}
protected void delete(HttpClient httpClient, String path)
throws CalDAV4JException {
DeleteMethod deleteMethod = new DeleteMethod(path);
try {
httpClient.executeMethod(hostConfiguration, deleteMethod);
} catch (Exception e){
throw new CalDAV4JException("Problem executing delete method",e);
}
if (deleteMethod.getStatusCode() != CaldavStatus.SC_NO_CONTENT){
MethodUtil.StatusToExceptions(deleteMethod);
throw new CalDAV4JException("Problem executing delete method");
}
cache.removeResource(getHref(path));
}
/**
* Replace double slashes
* @param relativePath
* @return a path with double slashes removed
*/
protected String getAbsolutePath(String relativePath){
return (getCalendarCollectionRoot() + "/" + relativePath).replaceAll("/+", "/");
}
/**
* retrieve etags using HEAD /path/to/resource.ics
*
* @param httpClient
* @param path
* @return
* @throws CalDAV4JException
*/
protected String getETag(HttpClient httpClient, String path) throws CalDAV4JException{
HeadMethod headMethod = new HeadMethod(path);
try {
httpClient.executeMethod(hostConfiguration, headMethod);
int statusCode = headMethod.getStatusCode();
if (statusCode == CaldavStatus.SC_NOT_FOUND) {
throw new ResourceNotFoundException(
ResourceNotFoundException.IdentifierType.PATH, path);
}
if (statusCode != CaldavStatus.SC_OK){
throw new CalDAV4JException(
"Unexpected Status returned from Server: "
+ headMethod.getStatusCode());
}
} catch (IOException e) {
String message = hostConfiguration.getHostURL()+ headMethod.getPath();
throw new CalDAV4JException("Problem executing HEAD method on: "+ message,e);
}
Header h = headMethod.getResponseHeader(HEADER_ETAG);
String etag = null;
if (h != null) {
etag = h.getValue();
}
return etag;
}
/**
* Useful for retrieving a list of UIDs of all events
*
* @param httpClient
* @param componentName
* @param propertyName
* @param query
* @return a list of property values of events.
* @throws CalDAV4JException
*
* @deprecated maybe create a method in ICalendarUtils or an "asString()" method
*/
protected List <String> getComponentProperty(HttpClient httpClient, String componentName, String propertyName, CalendarQuery query)
throws CalDAV4JException
{
List<String> propertyList = new ArrayList<String>();
List<Calendar> calendarList = getCalendarLight(httpClient, query);
for (Calendar cal : calendarList){
propertyList.add (
ICalendarUtils.getPropertyValue(
cal.getComponent(componentName), propertyName
)
);
}
return propertyList;
}
/**
* Return a list of components using REPORT
* @param query
* @return a new Calendar list with no elements if 0
* @throws CalDAV4JException
*/
public List<Calendar> queryCalendars(HttpClient httpClient, CalendarQuery query)
throws CalDAV4JException
{
List <Calendar> list = new ArrayList<Calendar>();
for (CalDAVResource cr: getCalDAVResources(httpClient, query)) {
list.add(cr.getCalendar());
}
return list;
}
/**
* return a list of components using REPORT without
* passing thru CaldavResource
* @param query
* @return
* @throws CalDAV4JException
* @deprecated This is still a proposed feature
*/
public List<Calendar> getCalendarLight(HttpClient httpClient, CalendarQuery query)
throws CalDAV4JException
{
List <Calendar> list = new ArrayList<Calendar>();
if (isCacheEnabled()) {
query.setCalendarDataProp(null);
}
CalDAVReportMethod reportMethod = methodFactory.createCalDAVReportMethod();
reportMethod.setPath(getCalendarCollectionRoot());
reportMethod.setReportRequest(query);
try {
httpClient.executeMethod(getHostConfiguration(), reportMethod);
} catch (Exception he) {
throw new CalDAV4JException("Problem executing method", he);
}
Enumeration<CalDAVResponse> e = reportMethod.getResponses();
while (e.hasMoreElements()){
CalDAVResponse response = e.nextElement();
String etag = response.getETag();
if (isCacheEnabled()) {
CalDAVResource resource = getCalDAVResource(httpClient,
stripHost(response.getHref()), etag);
Calendar cal = resource.getCalendar();
if ( !isGoogleTombstone(cal)) {
list.add(resource.getCalendar());
// XXX check if getCalDAVResource does its caching job
cache.putResource(resource);
}
} else {
if (response.getCalendarDataProperty() != null) {
list.add(response.getCalendar());
}
}
}
return list;
}
// FIXME test speed
public Enumeration<CalDAVResponse> getResponse(HttpClient httpClient, CalendarQuery query) throws CalDAV4JException
{
List <Calendar> list = new ArrayList<Calendar>();
if (isCacheEnabled()) {
query.setCalendarDataProp(null);
}
CalDAVReportMethod reportMethod = methodFactory.createCalDAVReportMethod();
reportMethod.setPath(getCalendarCollectionRoot());
reportMethod.setReportRequest(query);
try {
httpClient.executeMethod(getHostConfiguration(), reportMethod);
if (reportMethod.getStatusCode() >=400) {
throw new Exception(reportMethod.getStatusText());
}
} catch (Exception he) {
throw new CalDAV4JException("Problem executing method", he);
}
Enumeration<CalDAVResponse> e = reportMethod.getResponses();
return e;
}
/**
* return a list of caldav resources.
* All other methods should use this one
*
* The use of caching changes the behavior of this method.
* if cache is not enable, returns a list of CalDAVResource parsed from the response
* if cache is enabled, foreach HREF returned by server:
* - retrieve the resource using getCaldavReource(client, string), this method checks cache
* -
* @param httpClient
* @param componentName
* @param query
* @return
* @throws CalDAV4JException
*/
protected List<CalDAVResource> getCalDAVResources(HttpClient httpClient, CalendarQuery query)
throws CalDAV4JException
{
boolean usingCache = isCacheEnabled();
if (usingCache) {
query.setCalendarDataProp(null);
log.debug("Using cache, so I am removing calendar data");
}
log.trace("Executing query: " + GenerateQuery.printQuery(query));
CalDAVReportMethod reportMethod = methodFactory.createCalDAVReportMethod();
reportMethod.setPath(getCalendarCollectionRoot());
reportMethod.setReportRequest(query);
try {
httpClient.executeMethod(getHostConfiguration(), reportMethod);
} catch (ConnectException connEx) {
// TODO getHostURL is synchronized
throw new CalDAV4JException("Can't connecto to "+
getHostConfiguration().getHostURL(), connEx.getCause());
} catch (Exception he) {
throw new CalDAV4JException("Problem executing method", he);
}
log.trace("Parsing response.. " );
Enumeration<CalDAVResponse> responseEnum = reportMethod.getResponses();
List<CalDAVResource> list = new ArrayList<CalDAVResource>();
while (responseEnum.hasMoreElements()){
try {
CalDAVResponse response = responseEnum.nextElement();
String etag = response.getETag();
if (usingCache) {
CalDAVResource resource = getCalDAVResource(httpClient,
stripHost(response.getHref()), etag);
list.add(resource);
cache.putResource(resource);
/* dead code, to be reenabled in case of tombstones
// avoid parsing object if not required
if (isSkipGoogleTombstones()) {
list.add(resource);
cache.putResource(resource);
} else if (! isGoogleTombstone(resource.getCalendar())){
list.add(resource);
cache.putResource(resource);
}
*/
} else {
if (response != null) {
list.add(new CalDAVResource(response));
}
}
} catch (Exception e) {
log.error("Exception while retrieving objects:" + e.getMessage(), e);
}
}
return list;
}
//
// MultiGet queries
//
/**
*
* @param httpClient
* @param componentName
* @param query
* @return a list of Calendar, each followed by a status
* @throws CalDAV4JException
*/
protected List<Calendar> getComponentByMultiget(HttpClient httpClient, String componentName,CalendarMultiget query) throws CalDAV4JException
{
if (isCacheEnabled()) {
query.setCalendarDataProp(null);
}
CalDAVReportMethod reportMethod = methodFactory.createCalDAVReportMethod();
reportMethod.setPath(getCalendarCollectionRoot());
reportMethod.setReportRequest(query);
try {
httpClient.executeMethod(getHostConfiguration(), reportMethod);
} catch (Exception he) {
throw new CalDAV4JException("Problem executing method", he);
}
Enumeration<CalDAVResponse> e = reportMethod.getResponses();
List<Calendar> list = new ArrayList<Calendar>();
while (e.hasMoreElements()){
CalDAVResponse response = e.nextElement();
if (response.getStatusCode()==CaldavStatus.SC_OK){
CalDAVResource resource = null;
if (isCacheEnabled()) {
String etag = response.getETag();
try{
resource =
getCalDAVResource(httpClient, stripHost(response.getHref()), etag);
list.add(resource.getCalendar());
} catch(Exception e1) {
// TODO
e1.printStackTrace();
}
} else {
list.add(response.getCalendar());
}
}
}
return list;
}
/**
* implementing calendar multiget
* @link { http://tools.ietf.org/html/rfc4791#section-7.9 }
*
* @author rpolli
*
*<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-multiget xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<C:calendar-data/>
</D:prop>
<D:href>/bernard/work/abcd1.ics</D:href>
<D:href>/bernard/work/mtg1.ics</D:href>
</C:calendar-multiget>
*/
public List<Calendar> multigetCalendarUris(HttpClient httpClient,
List<String> calendarUris )
throws CalDAV4JException {
// first create the calendar query
CalendarMultiget query = new CalendarMultiget("C", "D");
CalendarData calendarData = new CalendarData("C");
query.addProperty(CalDAVConstants.PROP_GETETAG);
query.setCalendarDataProp(calendarData);
query.setHrefs(calendarUris);
return getComponentByMultiget(httpClient, Component.VEVENT, query);
}
//
// HEAD method, useful for testing connection
//
public int testConnection(HttpClient httpClient)
throws CalDAV4JException {
HeadMethod method = new HeadMethod();
method.setPath(getCalendarCollectionRoot());
try {
httpClient.executeMethod(hostConfiguration,method);
} catch (Exception e) {
throw new CalDAV4JException(e.getMessage(), new Throwable(e.getCause()));
}
switch (method.getStatusCode()) {
case CaldavStatus.SC_OK:
break;
default:
throw new BadStatusException(method.getStatusCode(), method.getName(), getCalendarCollectionRoot());
}
return method.getStatusCode();
}
//
// manage ACL TODO
//
public Ace[] getAces(HttpClient httpClient) throws CalDAV4JException{
return getAces(httpClient, null);
}
public Ace[] getAces(HttpClient httpClient, String path) throws CalDAV4JException{
PropFindMethod method = methodFactory.createPropFindMethod();
method.setPath(getCalendarCollectionRoot() + StringUtils.defaultString(path, ""));
method.setDepth(DepthSupport.DEPTH_0);
try {
PropProperty propfind = PropertyFactory.createProperty(PropertyFactory.PROPFIND);
PropProperty prop = PropertyFactory.createProperty(PropertyFactory.PROP);
prop.addChild(PropertyFactory.createProperty(PropertyFactory.ACL));
propfind.addChild(prop);
method.setPropFindRequest(propfind);
httpClient.executeMethod(getHostConfiguration(),method);
} catch (CalDAV4JException e) {
throw new RuntimeException("Error in source code", e);
} catch (Exception e) {
throw new CalDAV4JException("Error in PROPFIND " + getCalendarCollectionRoot(), e);
}
int status = method.getStatusCode();
switch (status) {
case CaldavStatus.SC_MULTI_STATUS:
return method.getAces(method.getPath());
default:
MethodUtil.StatusToExceptions(method);
return null;
}
}
public void setAces(HttpClient client, Ace[] aces, String path) throws CalDAV4JException{
AclMethod method = new AclMethod();
method.setPath(getCalendarCollectionRoot() + StringUtils.defaultString(path, ""));
for (Ace a: aces) {
method.addAce(a);
}
try {
client.executeMethod(method);
int status = method.getStatusCode();
switch (status) {
case CaldavStatus.SC_OK:
break;
case CaldavStatus.SC_NOT_FOUND:
throw new ResourceNotFoundException(IdentifierType.PATH, method.getPath());
case CaldavStatus.SC_UNAUTHORIZED:
default:
throw new BadStatusException(status, method.getName(), getCalendarCollectionRoot());
}
} catch (HttpException e) {
throw new CalDAV4JException("Error in ACL " + getCalendarCollectionRoot(), e);
} catch (IOException e) {
throw new CalDAV4JException("Error in ACL " + getCalendarCollectionRoot(), e);
}
}
/*
// if set to true, caldav4j will skip timezone-only calendars
// this should be set to false to find deleted events
private boolean skipGoogleTombstones = false;
public boolean isSkipGoogleTombstones() {
return skipGoogleTombstones;
}
public void setSkipGoogleTombstones(boolean skipGoogleTombstones) {
this.skipGoogleTombstones = skipGoogleTombstones;
}
*/
} //end of class