From e4460e159fe3f81a8aff248594f65deee9b51fd2 Mon Sep 17 00:00:00 2001 From: mguessan Date: Thu, 22 Jul 2010 23:10:24 +0000 Subject: [PATCH] Caldav: implement time-range request git-svn-id: http://svn.code.sf.net/p/davmail/code/trunk@1253 3d1905a2-6b24-0410-a738-b14d5a86fcbd --- src/java/davmail/caldav/CaldavConnection.java | 7 +- .../davmail/exchange/ExchangeSession.java | 61 ++++++- .../exchange/dav/DavExchangeSession.java | 8 +- src/java/davmail/exchange/dav/Field.java | 1 + .../exchange/ews/EwsExchangeSession.java | 7 +- src/java/davmail/exchange/ews/Field.java | 1 + src/test/davmail/caldav/TestCaldav.java | 164 ++++++++++++++++++ 7 files changed, 240 insertions(+), 9 deletions(-) create mode 100644 src/test/davmail/caldav/TestCaldav.java diff --git a/src/java/davmail/caldav/CaldavConnection.java b/src/java/davmail/caldav/CaldavConnection.java index 442d6799..4216cfae 100644 --- a/src/java/davmail/caldav/CaldavConnection.java +++ b/src/java/davmail/caldav/CaldavConnection.java @@ -687,7 +687,7 @@ public class CaldavConnection extends AbstractConnection { appendEventsResponses(response, request, events); } else { // TODO: handle contacts ? - events = session.getAllEvents(request.getFolderPath()); + events = session.searchEvents(request.getFolderPath(), request.timeRangeStart, request.timeRangeEnd); appendEventsResponses(response, request, events); } @@ -1201,6 +1201,8 @@ public class CaldavConnection extends AbstractConnection { protected final HashSet properties = new HashSet(); protected HashSet hrefs; protected boolean isMultiGet; + protected String timeRangeStart; + protected String timeRangeEnd; protected CaldavRequest(String command, String path, Map headers, String body) throws IOException { this.command = command; @@ -1376,6 +1378,9 @@ public class CaldavConnection extends AbstractConnection { } else if ("calendar-multiget".equals(currentElement) || "addressbook-multiget".equals(currentElement)) { isMultiGet = true; + } else if ("time-range".equals(currentElement)) { + timeRangeStart = streamReader.getAttributeValue(null, "start"); + timeRangeEnd = streamReader.getAttributeValue(null, "end"); } else if (inProperties) { properties.add(currentElement); } diff --git a/src/java/davmail/exchange/ExchangeSession.java b/src/java/davmail/exchange/ExchangeSession.java index e2323988..964ef047 100644 --- a/src/java/davmail/exchange/ExchangeSession.java +++ b/src/java/davmail/exchange/ExchangeSession.java @@ -187,7 +187,7 @@ public abstract class ExchangeSession { LOGGER.debug("Session " + this + " created"); } - protected static String formatSearchDate(Date date) { + public static String formatSearchDate(Date date) { SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS, Locale.ENGLISH); dateFormatter.setTimeZone(GMT_TIMEZONE); return dateFormatter.format(date); @@ -603,7 +603,10 @@ public abstract class ExchangeSession { public abstract MessageList searchMessages(String folderName, Set attributes, Condition condition) throws IOException; protected enum Operator { - Or, And, Not, IsEqualTo, IsGreaterThan, IsGreaterThanOrEqualTo, IsLessThan, IsNull, IsTrue, IsFalse, + Or, And, Not, IsEqualTo, + IsGreaterThan, IsGreaterThanOrEqualTo, + IsLessThan, IsLowerThanOrEqualTo, + IsNull, IsTrue, IsFalse, Like, StartsWith, Contains } @@ -796,6 +799,15 @@ public abstract class ExchangeSession { */ public abstract Condition lt(String attributeName, String value); + /** + * Lower than or equals condition. + * + * @param attributeName logical Exchange attribute name + * @param value attribute value + * @return condition + */ + public abstract Condition lte(String attributeName, String value); + /** * Contains condition. * @@ -2526,13 +2538,51 @@ public abstract class ExchangeSession { dateCondition = gt("dtstart", formatSearchDate(cal.getTime())); } + return searchEvents(folderPath, dateCondition); + } + + /** + * Search events between start and end. + * + * @param folderPath Exchange folder path + * @param timeRangeStart date range start in zulu format + * @param timeRangeEnd date range start in zulu format + * @return list of calendar events + * @throws IOException on error + */ + public List searchEvents(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException { + try { + SimpleDateFormat parser = getZuluDateFormat(); + ExchangeSession.MultiCondition andCondition = and(); + if (timeRangeStart != null) { + andCondition.add(gte("dtstart", formatSearchDate(parser.parse(timeRangeStart)))); + } + if (timeRangeEnd != null) { + andCondition.add(lte("dtend", formatSearchDate(parser.parse(timeRangeEnd)))); + } + andCondition.add(equals("instancetype", 0)); + return searchEvents(folderPath, ITEM_PROPERTIES, andCondition); + } catch (ParseException e) { + throw new IOException(e); + } + } + + /** + * Search calendar events in provided folder. + * + * @param folderPath Exchange folder path + * @param dateCondition date filter + * @return list of calendar events + * @throws IOException on error + */ + public List searchEvents(String folderPath, Condition dateCondition) throws IOException { + Condition privateCondition = null; if (isSharedFolder(folderPath)) { LOGGER.debug("Shared or public calendar: exclude private events"); privateCondition = equals("sensitivity", 0); } - - + // instancetype 0 single appointment / 1 master recurring appointment return searchEvents(folderPath, ITEM_PROPERTIES, and(or(isNull("instancetype"), equals("instancetype", 1), @@ -2541,7 +2591,6 @@ public abstract class ExchangeSession { privateCondition)); } - /** * Search calendar events or messages in provided folder matching the search query. * @@ -2551,7 +2600,7 @@ public abstract class ExchangeSession { * @return list of calendar messages as Event objects * @throws IOException on error */ - protected abstract List searchEvents(String folderPath, Set attributes, Condition condition) throws IOException; + public abstract List searchEvents(String folderPath, Set attributes, Condition condition) throws IOException; /** * convert vcf extension to EML. diff --git a/src/java/davmail/exchange/dav/DavExchangeSession.java b/src/java/davmail/exchange/dav/DavExchangeSession.java index 046ef629..125a5a14 100644 --- a/src/java/davmail/exchange/dav/DavExchangeSession.java +++ b/src/java/davmail/exchange/dav/DavExchangeSession.java @@ -398,6 +398,7 @@ public class DavExchangeSession extends ExchangeSession { operatorMap.put(Operator.IsEqualTo, " = "); operatorMap.put(Operator.IsGreaterThanOrEqualTo, " >= "); operatorMap.put(Operator.IsGreaterThan, " > "); + operatorMap.put(Operator.IsLowerThanOrEqualTo, " <= "); operatorMap.put(Operator.IsLessThan, " < "); operatorMap.put(Operator.Like, " like "); operatorMap.put(Operator.IsNull, " is null"); @@ -516,6 +517,11 @@ public class DavExchangeSession extends ExchangeSession { return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value); } + @Override + public Condition lte(String attributeName, String value) { + return new AttributeCondition(attributeName, Operator.IsLowerThanOrEqualTo, value); + } + @Override public Condition lt(String attributeName, String value) { return new AttributeCondition(attributeName, Operator.IsLessThan, value); @@ -1141,7 +1147,7 @@ public class DavExchangeSession extends ExchangeSession { } @Override - protected List searchEvents(String folderPath, Set attributes, Condition condition) throws IOException { + public List searchEvents(String folderPath, Set attributes, Condition condition) throws IOException { List events = new ArrayList(); MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow); for (MultiStatusResponse response : responses) { diff --git a/src/java/davmail/exchange/dav/Field.java b/src/java/davmail/exchange/dav/Field.java index 53e38a2c..05b52b43 100644 --- a/src/java/davmail/exchange/dav/Field.java +++ b/src/java/davmail/exchange/dav/Field.java @@ -166,6 +166,7 @@ public class Field { createField(SCHEMAS_EXCHANGE, "permanenturl"); createField(URN_SCHEMAS_CALENDAR, "instancetype"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:instancetype/Integer createField(URN_SCHEMAS_CALENDAR, "dtstart"); // 0x10C3 SystemTime + createField(URN_SCHEMAS_CALENDAR, "dtend"); // 0x10C4 SystemTime createField(SCHEMAS_EXCHANGE, "sensitivity"); // PR_SENSITIVITY 0x0036 Integer createField(URN_SCHEMAS_CALENDAR, "timezoneid"); // DistinguishedPropertySetType.PublicStrings/urn:schemas:calendar:timezoneid/Integer createField("processed", 0x65e8, PropertyType.Boolean);// PR_MESSAGE_PROCESSED diff --git a/src/java/davmail/exchange/ews/EwsExchangeSession.java b/src/java/davmail/exchange/ews/EwsExchangeSession.java index aa996b7a..f0c7add9 100644 --- a/src/java/davmail/exchange/ews/EwsExchangeSession.java +++ b/src/java/davmail/exchange/ews/EwsExchangeSession.java @@ -439,6 +439,11 @@ public class EwsExchangeSession extends ExchangeSession { return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value); } + @Override + public Condition lte(String attributeName, String value) { + return new AttributeCondition(attributeName, Operator.IsLowerThanOrEqualTo, value); + } + @Override public Condition lt(String attributeName, String value) { return new AttributeCondition(attributeName, Operator.IsLessThan, value); @@ -906,7 +911,7 @@ public class EwsExchangeSession extends ExchangeSession { } @Override - protected List searchEvents(String folderPath, Set attributes, Condition condition) throws IOException { + public List searchEvents(String folderPath, Set attributes, Condition condition) throws IOException { List events = new ArrayList(); List responses = searchItems(folderPath, attributes, condition, diff --git a/src/java/davmail/exchange/ews/Field.java b/src/java/davmail/exchange/ews/Field.java index f8f5c4da..496fdc68 100644 --- a/src/java/davmail/exchange/ews/Field.java +++ b/src/java/davmail/exchange/ews/Field.java @@ -46,6 +46,7 @@ public class Field { FIELD_MAP.put("permanenturl", new ExtendedFieldURI(0x670E, ExtendedFieldURI.PropertyType.String)); //PR_FLAT_URL_NAME FIELD_MAP.put("instancetype", new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.PublicStrings, "urn:schemas:calendar:instancetype")); FIELD_MAP.put("dtstart", new ExtendedFieldURI(0x10C3, ExtendedFieldURI.PropertyType.SystemTime)); + FIELD_MAP.put("dtstart", new ExtendedFieldURI(0x10C4, ExtendedFieldURI.PropertyType.SystemTime)); FIELD_MAP.put("mimeContent", new UnindexedFieldURI("item:MimeContent")); diff --git a/src/test/davmail/caldav/TestCaldav.java b/src/test/davmail/caldav/TestCaldav.java new file mode 100644 index 00000000..6cde1fbe --- /dev/null +++ b/src/test/davmail/caldav/TestCaldav.java @@ -0,0 +1,164 @@ +/* + * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway + * Copyright (C) 2010 Mickael Guessant + * + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package davmail.caldav; + +import davmail.AbstractDavMailTestCase; +import davmail.DavGateway; +import davmail.Settings; +import davmail.exchange.ExchangeSession; +import davmail.exchange.ExchangeSessionFactory; +import org.apache.commons.httpclient.*; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.RequestEntity; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.DavMethodBase; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Test Caldav listener. + */ +public class TestCaldav extends AbstractDavMailTestCase { + + class SearchReportMethod extends DavMethodBase { + SearchReportMethod(String path, String stringContent) throws UnsupportedEncodingException { + this(path, stringContent.getBytes("UTF-8")); + } + + SearchReportMethod(String path, final byte[] content) { + super(path); + setRequestEntity(new RequestEntity() { + + public boolean isRepeatable() { + return true; + } + + public void writeRequest(OutputStream outputStream) throws IOException { + outputStream.write(content); + } + + public long getContentLength() { + return content.length; + } + + public String getContentType() { + return "text/xml;charset=UTF-8"; + } + }); + } + + @Override + public String getName() { + return "REPORT"; + } + + @Override + protected boolean isSuccess(int statusCode) { + return statusCode == HttpStatus.SC_MULTI_STATUS; + } + } + + HttpClient httpClient; + + @Override + public void setUp() throws IOException { + super.setUp(); + if (httpClient == null) { + // start gateway + DavGateway.start(); + httpClient = new HttpClient(); + HostConfiguration hostConfig = httpClient.getHostConfiguration(); + URI httpURI = new URI("http://localhost:" + Settings.getProperty("davmail.caldavPort"), true); + hostConfig.setHost(httpURI); + AuthScope authScope = new AuthScope(null, -1); + httpClient.getState().setCredentials(authScope, new NTCredentials(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password"), "", "")); + } + if (session == null) { + session = ExchangeSessionFactory.getInstance(Settings.getProperty("davmail.username"), Settings.getProperty("davmail.password")); + } + } + + public void testGetRoot() throws IOException { + GetMethod method = new GetMethod("/"); + httpClient.executeMethod(method); + assertEquals(HttpStatus.SC_OK, method.getStatusCode()); + } + + public void testGetUserRoot() throws IOException { + GetMethod method = new GetMethod("/users/" + session.getEmail() + '/'); + httpClient.executeMethod(method); + assertEquals(HttpStatus.SC_OK, method.getStatusCode()); + } + + public void testGetCalendar() throws IOException { + GetMethod method = new GetMethod("/users/" + session.getEmail() + "/calendar/"); + httpClient.executeMethod(method); + assertEquals(HttpStatus.SC_OK, method.getStatusCode()); + } + + public void testReportCalendar() throws IOException, DavException { + SimpleDateFormat formatter = ExchangeSession.getZuluDateFormat(); + Calendar cal = Calendar.getInstance(); + Date end = cal.getTime(); + cal.add(Calendar.MONTH, -1); + Date start = cal.getTime(); + + StringBuilder buffer = new StringBuilder(); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + buffer.append(""); + SearchReportMethod method = new SearchReportMethod("/users/" + session.getEmail() + "/calendar/",buffer.toString()); + httpClient.executeMethod(method); + assertEquals(HttpStatus.SC_MULTI_STATUS, method.getStatusCode()); + MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + Set ITEM_PROPERTIES = new HashSet(); + ITEM_PROPERTIES.add("instancetype"); + List events = session.searchEvents("/users/" + session.getEmail() + "/calendar/", ITEM_PROPERTIES, + session.and( + session.and( + session.gt("dtstart", session.formatSearchDate(start)), + session.lt("dtend", session.formatSearchDate(end)) + ) + , session.or(session.equals("instancetype", 1), session.equals("instancetype", 0)) + ) + ); + + assertEquals(events.size(), responses.length); + } + +}