/* * * * Copyright (C) 2014 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * / */ package org.anhonesteffort.flock.webdav.caldav; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.data.CalendarOutputter; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.ConstraintViolationException; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.util.Calendars; import org.anhonesteffort.flock.util.guava.Optional; import org.anhonesteffort.flock.webdav.AbstractDavComponentCollection; import org.anhonesteffort.flock.webdav.ComponentETagPair; import org.anhonesteffort.flock.webdav.InvalidComponentException; import org.anhonesteffort.flock.webdav.MultiStatusResult; import org.anhonesteffort.flock.webdav.PropertyParseException; import org.anhonesteffort.flock.webdav.WebDavConstants; import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.MultiStatusResponse; import org.apache.jackrabbit.webdav.client.methods.PutMethod; import org.apache.jackrabbit.webdav.client.methods.ReportMethod; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; import org.apache.jackrabbit.webdav.property.DavPropertySet; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.security.report.PrincipalMatchReport; import org.apache.jackrabbit.webdav.version.report.ReportInfo; import org.apache.jackrabbit.webdav.version.report.ReportType; import org.apache.jackrabbit.webdav.xml.DomUtil; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; /** * Programmer: rhodey */ public class CalDavCollection extends AbstractDavComponentCollection<Calendar> implements DavCalendarCollection { public CalDavCollection(CalDavStore calDavStore, String path) { super(calDavStore, path); } public CalDavCollection(CalDavStore calDavStore, String path, String displayName, String description, Integer color) { super(calDavStore, path, displayName); properties.add(new DefaultDavProperty<String>(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, description)); properties.add(new DefaultDavProperty<Integer>(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, color)); } public CalDavCollection(CalDavStore calDavStore, String path, DavPropertySet properties) { super(calDavStore, path, properties); this.properties = properties; } @Override protected String getComponentPathFromUid(String uid) { return getPath().concat(uid.concat(CalDavConstants.ICAL_FILE_EXTENSION)); } @Override protected DavPropertyNameSet getPropertyNamesForFetch() { DavPropertyNameSet calendarProps = super.getPropertyNamesForFetch(); calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION); calendarProps.add(CalDavConstants.PROPERTY_NAME_SUPPORTED_CALENDAR_COMPONENT_SET); calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE); calendarProps.add(CalDavConstants.PROPERTY_NAME_SUPPORTED_CALENDAR_DATA); calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_ATTENDEES_PER_INSTANCE); calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_DATE_TIME); calendarProps.add(CalDavConstants.PROPERTY_NAME_MIN_DATE_TIME); calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_INSTANCES); calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_RESOURCE_SIZE); calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR); calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_ORDER); return calendarProps; } @Override protected ReportType getQueryReportType() { return ReportType.register(CalDavConstants.PROPERTY_CALENDAR_QUERY, CalDavConstants.CALDAV_NAMESPACE, PrincipalMatchReport.class); } @Override protected ReportType getMultiGetReportType() { return ReportType.register(CalDavConstants.PROPERTY_CALENDAR_MULTIGET, CalDavConstants.CALDAV_NAMESPACE, PrincipalMatchReport.class); } @Override protected DavPropertyNameSet getPropertyNamesForReports() { DavPropertyNameSet calendarProperties = new DavPropertyNameSet(); calendarProperties.add(CalDavConstants.PROPERTY_NAME_CALENDAR_DATA); calendarProperties.add(DavPropertyName.GETETAG); return calendarProperties; } @Override public Optional<String> getDescription() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, String.class); } @Override public void setDescription(String description) throws DavException, IOException { DavPropertySet updateProperties = new DavPropertySet(); updateProperties.add(new DefaultDavProperty<String>(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, description)); patchProperties(updateProperties, new DavPropertyNameSet()); } @Override public Optional<Calendar> getTimeZone() throws PropertyParseException { try { Optional<String> calendarTimeZone = getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, String.class); if (calendarTimeZone.isPresent()) return Optional.of(new CalendarBuilder().build(new StringReader(calendarTimeZone.get()))); } catch (IOException e) { throw new PropertyParseException("caught exception while building time zone.", getPath(), CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, e); } catch (ParserException e) { throw new PropertyParseException("caught exception while building time zone.", getPath(), CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, e); } return Optional.absent(); } @Override public void setTimeZone(Calendar timezone) throws DavException, IOException { DavPropertySet updateProperties = new DavPropertySet(); timezone.getProperties().add(new ProdId(((CalDavStore)getStore()).getProductId())); updateProperties.add(new DefaultDavProperty<String>(CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, timezone.toString())); patchProperties(updateProperties, new DavPropertyNameSet()); } @Override public List<String> getSupportedComponentSet() throws PropertyParseException { List<String> supportedComponents = new ArrayList<String>(); Optional<ArrayList> supportedCalCompSetProp = getProperty(CalDavConstants.PROPERTY_NAME_SUPPORTED_CALENDAR_COMPONENT_SET, ArrayList.class); if (supportedCalCompSetProp.isPresent()) { for (Node child : (ArrayList<Node>) supportedCalCompSetProp.get()) { if (child instanceof Element) { Node nameNode = child.getAttributes().getNamedItem(CalDavConstants.ATTRIBUTE_NAME); if (nameNode != null) supportedComponents.add(nameNode.getTextContent()); } } } return supportedComponents; } @Override public Optional<Long> getMaxResourceSize() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_MAX_RESOURCE_SIZE, Long.class); } @Override public Optional<String> getMinDateTime() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_MIN_DATE_TIME, String.class); } @Override public Optional<String> getMaxDateTime() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_MAX_DATE_TIME, String.class); } @Override public Optional<Integer> getMaxInstances() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_MAX_INSTANCES, Integer.class); } @Override public Optional<Integer> getMaxAttendeesPerInstance() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_MAX_ATTENDEES_PER_INSTANCE, Integer.class); } @Override public Optional<Integer> getColor() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, Integer.class); } @Override public void setColor(int color) throws DavException, IOException { DavPropertySet updateProperties = new DavPropertySet(); updateProperties.add(new DefaultDavProperty<Integer>(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, color)); patchProperties(updateProperties, new DavPropertyNameSet()); } @Override public Optional<Integer> getOrder() throws PropertyParseException { return getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_ORDER, Integer.class); } @Override public void setOrder(Integer order) throws DavException, IOException { DavPropertySet updateProperties = new DavPropertySet(); updateProperties.add(new DefaultDavProperty<Integer>(CalDavConstants.PROPERTY_NAME_CALENDAR_ORDER, order)); patchProperties(updateProperties, new DavPropertyNameSet()); } @Override protected MultiStatusResult<Calendar> getComponentsFromMultiStatus(MultiStatusResponse[] msResponses) { List<ComponentETagPair<Calendar>> calendars = new LinkedList<ComponentETagPair<Calendar>>(); List<InvalidComponentException> exceptions = new LinkedList<InvalidComponentException>(); for (MultiStatusResponse response : msResponses) { Calendar calendar = null; String eTag = null; DavPropertySet propertySet = response.getProperties(WebDavConstants.SC_OK); if (propertySet.get(CalDavConstants.PROPERTY_NAME_CALENDAR_DATA) != null) { String calendarData = (String) propertySet.get(CalDavConstants.PROPERTY_NAME_CALENDAR_DATA).getValue(); // OwnCloud :( if (!calendarData.contains("\r")) calendarData = calendarData.replace("\n", "\r\n"); try { calendar = new CalendarBuilder().build(new StringReader(calendarData)); } catch (IOException e) { exceptions.add( new InvalidComponentException("Caught exception while parsing MultiStatus", CalDavConstants.CALDAV_NAMESPACE, getPath(), e) ); } catch (ParserException e) { exceptions.add( new InvalidComponentException("Caught exception while parsing MultiStatus", CalDavConstants.CALDAV_NAMESPACE, getPath(), e) ); } } if (propertySet.get(DavPropertyName.GETETAG) != null) eTag = (String) propertySet.get(DavPropertyName.GETETAG).getValue(); if (calendar != null) calendars.add(new ComponentETagPair<Calendar>(calendar, Optional.fromNullable(eTag))); } return new MultiStatusResult<Calendar>(calendars, exceptions); } private MultiStatusResult<Calendar> getComponentsByType(String componentType) throws DavException, IOException { try { Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); Element resourceFilter = DomUtil.createElement(document, CalDavConstants.PROPERTY_FILTER, CalDavConstants.CALDAV_NAMESPACE); Element calendarFilter = DomUtil.createElement(document, CalDavConstants.PROPERTY_COMP_FILTER, CalDavConstants.CALDAV_NAMESPACE); Element componentFilter = DomUtil.createElement(document, CalDavConstants.PROPERTY_COMP_FILTER, CalDavConstants.CALDAV_NAMESPACE); componentFilter.setAttribute(CalDavConstants.ATTRIBUTE_NAME, componentType); calendarFilter.setAttribute(CalDavConstants.ATTRIBUTE_NAME, Calendar.VCALENDAR); calendarFilter.appendChild(componentFilter); resourceFilter.appendChild(calendarFilter); ReportInfo reportInfo = new ReportInfo(getQueryReportType(), 1, getPropertyNamesForReports()); reportInfo.setContentElement(resourceFilter); ReportMethod reportMethod = new ReportMethod(getPath(), reportInfo); try { client.execute(reportMethod); if (reportMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) return getComponentsFromMultiStatus(reportMethod.getResponseBodyAsMultiStatus().getResponses()); throw new DavException(reportMethod.getStatusCode(), reportMethod.getStatusText()); } finally { reportMethod.releaseConnection(); } } catch (ParserConfigurationException e) { throw new IOException("Caught exception while building document.", e); } } public MultiStatusResult<Calendar> getEventComponents() throws DavException, IOException { return getComponentsByType(Component.VEVENT); } public MultiStatusResult<Calendar> getToDoComponents() throws DavException, IOException { return getComponentsByType(Component.VTODO); } @Override protected void putComponentToServer(Calendar calendar, Optional<String> ifMatchETag) throws InvalidComponentException, DavException, IOException { calendar.getProperties().remove(ProdId.PRODID); calendar.getProperties().add(new ProdId(((CalDavStore)getStore()).getProductId())); try { if (Calendars.getUid(calendar) == null) throw new InvalidComponentException("Cannot put iCal to server without UID!", CalDavConstants.CALDAV_NAMESPACE, getPath()); String calendarUid = Calendars.getUid(calendar).getValue(); PutMethod putMethod = new PutMethod(getComponentPathFromUid(calendarUid)); if (ifMatchETag.isPresent()) putMethod.addRequestHeader("If-Match", ifMatchETag.get()); // TODO: constant for this. else putMethod.addRequestHeader("If-None-Match", "*"); // TODO: constant for this. try { CalendarOutputter calendarOutputter = new CalendarOutputter(); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); calendarOutputter.output(calendar, byteStream); putMethod.setRequestEntity(new ByteArrayRequestEntity(byteStream.toByteArray(), CalDavConstants.HEADER_CONTENT_TYPE_CALENDAR)); client.execute(putMethod); int status = putMethod.getStatusCode(); if (status == WebDavConstants.SC_REQUEST_ENTITY_TOO_LARGE || status == WebDavConstants.SC_FORBIDDEN) { throw new InvalidComponentException("Put method returned bad status " + status, CalDavConstants.CALDAV_NAMESPACE, getPath(), calendarUid); } if (status < WebDavConstants.SC_OK || status > WebDavConstants.SC_NO_CONTENT) { throw new DavException(status, putMethod.getStatusText()); } } finally { putMethod.releaseConnection(); } } catch (ConstraintViolationException e) { throw new InvalidComponentException("Caught exception while parsing UID from calendar", CalDavConstants.CALDAV_NAMESPACE, getPath(), e); } catch (ValidationException e) { throw new InvalidComponentException("Caught exception whie outputting calendar to stream", CalDavConstants.CALDAV_NAMESPACE, getPath(), e); } } }