1
0
mirror of https://github.com/etesync/android synced 2024-11-26 01:48:34 +00:00

refactoring, RoboHydra tests

This commit is contained in:
rfc2822 2013-11-03 14:52:49 +01:00
parent 0626f1ecdc
commit 362f0036be
21 changed files with 740 additions and 380 deletions

View File

@ -9,7 +9,7 @@ package at.bitfire.davdroid;
public class Constants {
public static final String
APP_VERSION = "0.3.6-alpha",
APP_VERSION = "0.3.7-alpha",
ACCOUNT_TYPE = "bitfire.at.davdroid",

View File

@ -3,8 +3,8 @@ package at.bitfire.davdroid;
import java.net.URI;
import java.net.URISyntaxException;
public class Utils {
public static boolean isSameURL(URI a, URI b) {
public class URIUtils {
public static boolean isSame(URI a, URI b) {
try {
a = new URI(a.getScheme(), null, a.getHost(), a.getPort(), a.getPath(), a.getQuery(), a.getFragment());
b = new URI(b.getScheme(), null, b.getHost(), b.getPort(), b.getPath(), b.getQuery(), b.getFragment());
@ -14,8 +14,8 @@ public class Utils {
}
}
public static URI resolveURI(URI parent, String member) {
if (!member.startsWith("/") && !member.startsWith("http:") && !member.startsWith("https://"))
public static URI resolve(URI parent, String member) {
if (!member.startsWith("/") && !member.startsWith("http:") && !member.startsWith("https:"))
member = "./" + member;
return parent.resolve(member);

View File

@ -7,10 +7,9 @@
******************************************************************************/
package at.bitfire.davdroid.resource;
import java.io.IOException;
import java.net.URISyntaxException;
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
import at.bitfire.davdroid.webdav.WebDavResource.MultigetType;
public class CalDavCalendar extends RemoteCollection<Event> {
//private final static String TAG = "davdroid.CalDavCalendar";

View File

@ -9,7 +9,7 @@ package at.bitfire.davdroid.resource;
import java.net.URISyntaxException;
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
import at.bitfire.davdroid.webdav.WebDavResource.MultigetType;
public class CardDavAddressBook extends RemoteCollection<Contact> {
//private final static String TAG = "davdroid.CardDavAddressBook";

View File

@ -23,22 +23,21 @@ import org.apache.http.HttpException;
import android.util.Log;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.InvalidDavResponseException;
import at.bitfire.davdroid.webdav.WebDavCollection;
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
import at.bitfire.davdroid.webdav.WebDavResource;
import at.bitfire.davdroid.webdav.WebDavResource.MultigetType;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
public abstract class RemoteCollection<ResourceType extends Resource> {
private static final String TAG = "davdroid.RemoteCollection";
@Getter WebDavCollection collection;
@Getter WebDavResource collection;
abstract protected String memberContentType();
abstract protected MultigetType multiGetType();
abstract protected ResourceType newResourceSkeleton(String name, String ETag);
public RemoteCollection(String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
collection = new WebDavCollection(new URI(baseURL), user, password, preemptiveAuth);
collection = new WebDavResource(new URI(baseURL), user, password, preemptiveAuth, true);
}
@ -58,9 +57,12 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
List<ResourceType> resources = new LinkedList<ResourceType>();
for (WebDavResource member : collection.getMembers())
resources.add(newResourceSkeleton(member.getName(), member.getETag()));
return resources.toArray(new Resource[0]);
if (collection.getMembers() != null) {
for (WebDavResource member : collection.getMembers())
resources.add(newResourceSkeleton(member.getName(), member.getETag()));
return resources.toArray(new Resource[0]);
} else
return null;
}
@SuppressWarnings("unchecked")
@ -78,19 +80,23 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
collection.multiGet(names.toArray(new String[0]), multiGetType());
LinkedList<ResourceType> foundResources = new LinkedList<ResourceType>();
for (WebDavResource member : collection.getMembers()) {
ResourceType resource = newResourceSkeleton(member.getName(), member.getETag());
try {
InputStream is = member.getContent();
if (is != null) {
resource.parseEntity(is);
foundResources.add(resource);
} else
Log.e(TAG, "Ignoring entity without content");
} catch (ParserException ex) {
Log.e(TAG, "Ignoring unparseable entity in multi-response", ex);
if (collection.getMembers() != null)
for (WebDavResource member : collection.getMembers()) {
ResourceType resource = newResourceSkeleton(member.getName(), member.getETag());
try {
InputStream is = member.getContent();
if (is != null) {
resource.parseEntity(is);
foundResources.add(resource);
} else
Log.e(TAG, "Ignoring entity without content");
} catch (ParserException ex) {
Log.e(TAG, "Ignoring unparseable entity in multi-response", ex);
}
}
}
else
return null;
return foundResources.toArray(new Resource[0]);
} catch (ParserException ex) {
Log.w(TAG, "Couldn't parse single multi-get entity", ex);
@ -113,16 +119,22 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
member.setContentType(memberContentType());
member.put(res.toEntity().getBytes("UTF-8"), PutMode.ADD_DONT_OVERWRITE);
collection.invalidateCTag();
}
public void delete(Resource res) throws IOException, HttpException {
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
member.delete();
collection.invalidateCTag();
}
public void update(Resource res) throws IOException, HttpException, ValidationException {
WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag());
member.setContentType(memberContentType());
member.put(res.toEntity().getBytes("UTF-8"), PutMode.UPDATE_DONT_OVERWRITE);
collection.invalidateCTag();
}
}

View File

@ -30,7 +30,6 @@ import android.widget.Toast;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.resource.IncapableResourceException;
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
import at.bitfire.davdroid.webdav.WebDavCollection;
import at.bitfire.davdroid.webdav.WebDavResource;
public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks<ServerInfo> {
@ -110,13 +109,15 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
try {
// (1/5) detect capabilities
WebDavCollection base = new WebDavCollection(new URI(serverInfo.getBaseURL()), serverInfo.getUserName(),
serverInfo.getPassword(), serverInfo.isAuthPreemptive());
WebDavResource base = new WebDavResource(new URI(serverInfo.getBaseURL()), serverInfo.getUserName(),
serverInfo.getPassword(), serverInfo.isAuthPreemptive(), true);
base.options();
serverInfo.setCardDAV(base.supportsDAV("addressbook"));
serverInfo.setCalDAV(base.supportsDAV("calendar-access"));
if (!base.supportsMethod("PROPFIND") || (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()))
if (!base.supportsMethod("PROPFIND") || !base.supportsMethod("GET") ||
!base.supportsMethod("PUT") || !base.supportsMethod("DELETE") ||
(!serverInfo.isCalDAV() && !serverInfo.isCardDAV()))
throw new IncapableResourceException(getContext().getString(R.string.neither_caldav_nor_carddav));
// (2/5) get principal URL
@ -129,7 +130,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
throw new IncapableResourceException(getContext().getString(R.string.error_principal_path));
// (3/5) get home sets
WebDavCollection principal = new WebDavCollection(base, principalPath);
WebDavResource principal = new WebDavResource(base, principalPath);
principal.propfind(Mode.HOME_SETS);
String pathAddressBooks = null;
@ -152,42 +153,44 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
// (4/5) get address books
if (serverInfo.isCardDAV()) {
WebDavCollection homeSetAddressBooks = new WebDavCollection(principal, pathAddressBooks);
WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks);
homeSetAddressBooks.propfind(Mode.MEMBERS_COLLECTIONS);
List<ServerInfo.ResourceInfo> addressBooks = new LinkedList<ServerInfo.ResourceInfo>();
for (WebDavResource resource : homeSetAddressBooks.getMembers())
if (resource.isAddressBook()) {
Log.i(TAG, "Found address book: " + resource.getLocation().getPath());
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
resource.getLocation().getPath(),
resource.getDisplayName(),
resource.getDescription()
);
addressBooks.add(info);
}
if (homeSetAddressBooks.getMembers() != null)
for (WebDavResource resource : homeSetAddressBooks.getMembers())
if (resource.isAddressBook()) {
Log.i(TAG, "Found address book: " + resource.getLocation().getPath());
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
ServerInfo.ResourceInfo.Type.ADDRESS_BOOK,
resource.getLocation().getPath(),
resource.getDisplayName(),
resource.getDescription()
);
addressBooks.add(info);
}
serverInfo.setAddressBooks(addressBooks);
}
// (5/5) get calendars
if (serverInfo.isCalDAV()) {
WebDavCollection homeSetCalendars = new WebDavCollection(principal, pathCalendars);
WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars);
homeSetCalendars.propfind(Mode.MEMBERS_COLLECTIONS);
List<ServerInfo.ResourceInfo> calendars = new LinkedList<ServerInfo.ResourceInfo>();
for (WebDavResource resource : homeSetCalendars.getMembers())
if (resource.isCalendar()) {
Log.i(TAG, "Found calendar: " + resource.getLocation().getPath());
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
ServerInfo.ResourceInfo.Type.CALENDAR,
resource.getLocation().getPath(),
resource.getDisplayName(),
resource.getDescription()
);
calendars.add(info);
}
if (homeSetCalendars.getMembers() != null)
for (WebDavResource resource : homeSetCalendars.getMembers())
if (resource.isCalendar()) {
Log.i(TAG, "Found calendar: " + resource.getLocation().getPath());
ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo(
ServerInfo.ResourceInfo.Type.CALENDAR,
resource.getLocation().getPath(),
resource.getDisplayName(),
resource.getDescription()
);
calendars.add(info);
}
serverInfo.setCalendars(calendars);
}

View File

@ -74,8 +74,8 @@ public class HttpPropfind extends HttpEntityEnclosingRequestBase {
setEntity(new StringEntity(writer.toString(), "UTF-8"));
Log.d(TAG, "Prepared PROPFIND request: " + writer.toString());
} catch(Exception e) {
Log.w(TAG, e.getMessage());
} catch(Exception ex) {
Log.e(TAG, "Couldn't prepare PROPFIND request", ex);
abort();
}
}

View File

@ -1,232 +0,0 @@
/*******************************************************************************
* Copyright (c) 2013 Richard Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
******************************************************************************/
package at.bitfire.davdroid.webdav;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import lombok.Getter;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicLineParser;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import android.util.Log;
import at.bitfire.davdroid.Utils;
public class WebDavCollection extends WebDavResource {
private static final String TAG = "davdroid.WebDavCollection";
public enum MultigetType {
ADDRESS_BOOK,
CALENDAR
}
/* list of resource members, empty until filled by propfind() or multiGet() */
@Getter protected List<WebDavResource> members = new LinkedList<WebDavResource>();
public WebDavCollection(URI baseURL, String username, String password, boolean preemptiveAuth) throws URISyntaxException {
super(baseURL, username, password, preemptiveAuth, true);
}
public WebDavCollection(WebDavCollection parent, URI member) {
super(parent, member);
}
public WebDavCollection(WebDavCollection parent, String member) {
super(parent, member);
}
/* collection operations */
public boolean propfind(HttpPropfind.Mode mode) throws IOException, InvalidDavResponseException, HttpException {
HttpPropfind propfind = new HttpPropfind(location, mode);
HttpResponse response = client.execute(propfind);
checkResponse(response);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_MULTI_STATUS) {
DavMultistatus multistatus;
try {
Serializer serializer = new Persister();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = new TeeInputStream(response.getEntity().getContent(), baos);
multistatus = serializer.read(DavMultistatus.class, is, false);
Log.d(TAG, "Received multistatus response: " + baos.toString("UTF-8"));
} catch (Exception ex) {
Log.w(TAG, "Invalid PROPFIND XML response", ex);
throw new InvalidDavResponseException();
}
processMultiStatus(multistatus);
return true;
} else
return false;
}
public boolean multiGet(String[] names, MultigetType type) throws IOException, InvalidDavResponseException, HttpException {
DavMultiget multiget = (type == MultigetType.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget();
multiget.prop = new DavProp();
multiget.prop.getetag = new DavProp.DavPropGetETag();
if (type == MultigetType.ADDRESS_BOOK)
multiget.prop.addressData = new DavProp.DavPropAddressData();
else if (type == MultigetType.CALENDAR)
multiget.prop.calendarData = new DavProp.DavPropCalendarData();
multiget.hrefs = new ArrayList<DavHref>(names.length);
for (String name : names) {
if (!name.startsWith("/")) name = "./" + name; // allow colons in relative URLs (see issue #45)
multiget.hrefs.add(new DavHref(Utils.resolveURI(location, name).getPath()));
}
Serializer serializer = new Persister();
StringWriter writer = new StringWriter();
try {
serializer.write(multiget, writer);
} catch (Exception e) {
Log.e(TAG, e.getLocalizedMessage());
return false;
}
HttpReport report = new HttpReport(location, writer.toString());
HttpResponse response = client.execute(report);
checkResponse(response);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_MULTI_STATUS) {
DavMultistatus multistatus;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = new TeeInputStream(response.getEntity().getContent(), baos);
multistatus = serializer.read(DavMultistatus.class, is, false);
Log.d(TAG, "Received multistatus response: " + baos.toString("UTF-8"));
} catch (Exception e) {
Log.e(TAG, e.getLocalizedMessage());
return false;
}
processMultiStatus(multistatus);
} else
throw new InvalidDavResponseException();
return true;
}
/* member operations */
@Override
public void put(byte[] data, PutMode mode) throws IOException, HttpException {
properties.remove(Property.CTAG);
super.put(data, mode);
}
@Override
public void delete() throws IOException, HttpException {
properties.remove(Property.CTAG);
super.delete();
}
/* HTTP support */
protected void processMultiStatus(DavMultistatus multistatus) throws HttpException {
if (multistatus.response == null) // empty response
return;
// member list will be built from response
members.clear();
for (DavResponse singleResponse : multistatus.response) {
URI href;
try {
href = Utils.resolveURI(location, singleResponse.getHref().href);
} catch(IllegalArgumentException ex) {
Log.w(TAG, "Ignoring illegal member URI in multi-status response", ex);
continue;
}
// about which resource is this response?
WebDavResource referenced = null;
if (Utils.isSameURL(location, href)) { // -> ourselves
referenced = this;
} else { // -> about a member
referenced = new WebDavResource(this, href);
members.add(referenced);
}
for (DavPropstat singlePropstat : singleResponse.getPropstat()) {
StatusLine status = BasicLineParser.parseStatusLine(singlePropstat.status, new BasicLineParser());
// ignore information about missing properties etc.
if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2)
continue;
DavProp prop = singlePropstat.prop;
if (prop.currentUserPrincipal != null)
referenced.properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href);
if (prop.addressbookHomeSet != null)
referenced.properties.put(Property.ADDRESSBOOK_HOMESET, prop.addressbookHomeSet.getHref().href);
if (singlePropstat.prop.calendarHomeSet != null)
referenced.properties.put(Property.CALENDAR_HOMESET, prop.calendarHomeSet.getHref().href);
if (prop.displayname != null)
referenced.properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName());
if (prop.resourcetype != null) {
if (prop.resourcetype.getAddressbook() != null) {
referenced.properties.put(Property.IS_ADDRESSBOOK, "1");
if (prop.addressbookDescription != null)
referenced.properties.put(Property.DESCRIPTION, prop.addressbookDescription.getDescription());
} else
referenced.properties.remove(Property.IS_ADDRESSBOOK);
if (prop.resourcetype.getCalendar() != null) {
referenced.properties.put(Property.IS_CALENDAR, "1");
if (prop.calendarDescription != null)
referenced.properties.put(Property.DESCRIPTION, prop.calendarDescription.getDescription());
} else
referenced.properties.remove(Property.IS_CALENDAR);
}
if (prop.getctag != null)
referenced.properties.put(Property.CTAG, prop.getctag.getCTag());
if (prop.getetag != null)
referenced.properties.put(Property.ETAG, prop.getetag.getETag());
if (prop.calendarData != null)
referenced.content = new ByteArrayInputStream(prop.calendarData.ical.getBytes());
else if (prop.addressData != null)
referenced.content = new ByteArrayInputStream(prop.addressData.vcard.getBytes());
}
}
}
}

View File

@ -7,18 +7,25 @@
******************************************************************************/
package at.bitfire.davdroid.webdav;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpException;
@ -35,11 +42,14 @@ import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicLineParser;
import org.apache.http.params.CoreProtocolPNames;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import android.util.Log;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.Utils;
import at.bitfire.davdroid.URIUtils;
@ToString
@ -54,29 +64,54 @@ public class WebDavResource {
CTAG, ETAG,
CONTENT_TYPE
}
public enum MultigetType {
ADDRESS_BOOK,
CALENDAR
}
public enum PutMode {
ADD_DONT_OVERWRITE,
UPDATE_DONT_OVERWRITE
}
// location of this resource
@Getter protected URI location;
protected Set<String> capabilities = new HashSet<String>(), methods = new HashSet<String>();
// DAV capabilities (DAV: header) and allowed DAV methods (set for OPTIONS request)
protected Set<String> capabilities = new HashSet<String>(),
methods = new HashSet<String>();
// DAV properties
protected HashMap<Property, String> properties = new HashMap<Property, String>();
// list of members (only for collections)
@Getter protected List<WebDavResource> members;
// content (available after GET)
@Getter protected InputStream content;
protected DefaultHttpClient client;
public WebDavResource(URI baseURL, String username, String password, boolean preemptive, boolean isCollection) throws URISyntaxException {
public WebDavResource(URI baseURL, boolean trailingSlash) throws URISyntaxException {
location = baseURL.normalize();
if (isCollection && !location.getPath().endsWith("/"))
if (trailingSlash && !location.getPath().endsWith("/"))
location = new URI(location.getScheme(), location.getSchemeSpecificPart() + "/", null);
// create new HTTP client
client = new DefaultHttpClient();
client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, "DAVdroid/" + Constants.APP_VERSION);
// allow gzip compression
GzipDecompressingEntity.enable(client);
// redirections
client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
}
public WebDavResource(URI baseURL, String username, String password, boolean preemptive, boolean trailingSlash) throws URISyntaxException {
this(baseURL, trailingSlash);
// authenticate
client.getCredentialsProvider().setCredentials(new AuthScope(location.getHost(), location.getPort()),
new UsernamePasswordCredentials(username, password));
@ -85,29 +120,24 @@ public class WebDavResource {
Log.i(TAG, "Using preemptive Basic Authentication");
client.addRequestInterceptor(new PreemptiveAuthInterceptor(), 0);
}
// allow gzip compression
GzipDecompressingEntity.enable(client);
// redirections
client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
}
protected WebDavResource(WebDavCollection parent, URI uri) {
protected WebDavResource(WebDavResource parent, URI uri) {
location = uri;
client = parent.client;
}
public WebDavResource(WebDavCollection parent, String member) {
location = Utils.resolveURI(parent.location, member);
public WebDavResource(WebDavResource parent, String member) {
location = URIUtils.resolve(parent.location, member);
client = parent.client;
}
public WebDavResource(WebDavCollection parent, String member, String ETag) {
public WebDavResource(WebDavResource parent, String member, String ETag) {
this(parent, member);
properties.put(Property.ETAG, ETag);
}
protected void checkResponse(HttpResponse response) throws HttpException {
checkResponse(response.getStatusLine());
}
@ -140,14 +170,12 @@ public class WebDavResource {
checkResponse(response);
Header[] allowHeaders = response.getHeaders("Allow");
if (allowHeaders != null)
for (Header allowHeader : allowHeaders)
methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?")));
for (Header allowHeader : allowHeaders)
methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?")));
Header[] capHeaders = response.getHeaders("DAV");
if (capHeaders != null)
for (Header capHeader : capHeaders)
capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?")));
for (Header capHeader : capHeaders)
capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?")));
}
public boolean supportsDAV(String capability) {
@ -192,6 +220,9 @@ public class WebDavResource {
public String getCTag() {
return properties.get(Property.CTAG);
}
public void invalidateCTag() {
properties.remove(Property.CTAG);
}
public String getETag() {
return properties.get(Property.ETAG);
@ -214,6 +245,81 @@ public class WebDavResource {
}
/* collection operations */
public boolean propfind(HttpPropfind.Mode mode) throws IOException, InvalidDavResponseException, HttpException {
HttpPropfind propfind = new HttpPropfind(location, mode);
HttpResponse response = client.execute(propfind);
checkResponse(response);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_MULTI_STATUS) {
DavMultistatus multistatus;
try {
Serializer serializer = new Persister();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = new TeeInputStream(response.getEntity().getContent(), baos);
multistatus = serializer.read(DavMultistatus.class, is, false);
Log.d(TAG, "Received multistatus response: " + baos.toString("UTF-8"));
} catch (Exception ex) {
Log.w(TAG, "Invalid PROPFIND XML response", ex);
throw new InvalidDavResponseException();
}
processMultiStatus(multistatus);
return true;
} else
return false;
}
public boolean multiGet(String[] names, MultigetType type) throws IOException, InvalidDavResponseException, HttpException {
DavMultiget multiget = (type == MultigetType.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget();
multiget.prop = new DavProp();
multiget.prop.getetag = new DavProp.DavPropGetETag();
if (type == MultigetType.ADDRESS_BOOK)
multiget.prop.addressData = new DavProp.DavPropAddressData();
else if (type == MultigetType.CALENDAR)
multiget.prop.calendarData = new DavProp.DavPropCalendarData();
multiget.hrefs = new ArrayList<DavHref>(names.length);
for (String name : names)
multiget.hrefs.add(new DavHref(URIUtils.resolve(location, name).getPath()));
Serializer serializer = new Persister();
StringWriter writer = new StringWriter();
try {
serializer.write(multiget, writer);
} catch (Exception e) {
Log.e(TAG, e.getLocalizedMessage());
return false;
}
HttpReport report = new HttpReport(location, writer.toString());
HttpResponse response = client.execute(report);
checkResponse(response);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_MULTI_STATUS) {
DavMultistatus multistatus;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = new TeeInputStream(response.getEntity().getContent(), baos);
multistatus = serializer.read(DavMultistatus.class, is, false);
Log.d(TAG, "Received multistatus response: " + baos.toString("UTF-8"));
} catch (Exception e) {
Log.e(TAG, e.getLocalizedMessage());
return false;
}
processMultiStatus(multistatus);
} else
throw new InvalidDavResponseException();
return true;
}
/* resource operations */
public void get() throws IOException, HttpException {
@ -233,13 +339,13 @@ public class WebDavResource {
put.addHeader("If-None-Match", "*");
break;
case UPDATE_DONT_OVERWRITE:
put.addHeader("If-Match", getETag());
put.addHeader("If-Match", (getETag() != null) ? getETag() : "*");
break;
}
if (getContentType() != null)
put.addHeader("Content-Type", getContentType());
checkResponse(client.execute(put));
}
@ -252,4 +358,87 @@ public class WebDavResource {
checkResponse(client.execute(delete));
}
/* helpers */
protected void processMultiStatus(DavMultistatus multistatus) throws HttpException {
if (multistatus.response == null) // empty response
return;
// member list will be built from response
List<WebDavResource> members = new LinkedList<WebDavResource>();
for (DavResponse singleResponse : multistatus.response) {
URI href;
try {
href = URIUtils.resolve(location, singleResponse.getHref().href);
} catch(IllegalArgumentException ex) {
Log.w(TAG, "Ignoring illegal member URI in multi-status response", ex);
continue;
}
// about which resource is this response?
WebDavResource referenced = null;
if (URIUtils.isSame(location, href)) { // -> ourselves
referenced = this;
} else { // -> about a member
referenced = new WebDavResource(this, href);
members.add(referenced);
}
for (DavPropstat singlePropstat : singleResponse.getPropstat()) {
StatusLine status = BasicLineParser.parseStatusLine(singlePropstat.status, new BasicLineParser());
// ignore information about missing properties etc.
if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2)
continue;
DavProp prop = singlePropstat.prop;
if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null)
referenced.properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href);
if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null)
referenced.properties.put(Property.ADDRESSBOOK_HOMESET, prop.addressbookHomeSet.getHref().href);
if (singlePropstat.prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null)
referenced.properties.put(Property.CALENDAR_HOMESET, prop.calendarHomeSet.getHref().href);
if (prop.displayname != null)
referenced.properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName());
if (prop.resourcetype != null) {
if (prop.resourcetype.getAddressbook() != null) {
referenced.properties.put(Property.IS_ADDRESSBOOK, "1");
if (prop.addressbookDescription != null)
referenced.properties.put(Property.DESCRIPTION, prop.addressbookDescription.getDescription());
} else
referenced.properties.remove(Property.IS_ADDRESSBOOK);
if (prop.resourcetype.getCalendar() != null) {
referenced.properties.put(Property.IS_CALENDAR, "1");
if (prop.calendarDescription != null)
referenced.properties.put(Property.DESCRIPTION, prop.calendarDescription.getDescription());
} else
referenced.properties.remove(Property.IS_CALENDAR);
}
if (prop.getctag != null)
referenced.properties.put(Property.CTAG, prop.getctag.getCTag());
if (prop.getetag != null)
referenced.properties.put(Property.ETAG, prop.getetag.getETag());
if (prop.calendarData != null)
referenced.content = new ByteArrayInputStream(prop.calendarData.ical.getBytes());
else if (prop.addressData != null)
referenced.content = new ByteArrayInputStream(prop.addressData.vcard.getBytes());
}
}
this.members = members;
}
}

BIN
test/assets/test.random Normal file

Binary file not shown.

1
test/robohydra/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,5 @@
{"plugins":[
"assets",
"redirect",
"dav-default"
]}

View File

@ -0,0 +1,12 @@
var RoboHydraHeadFilesystem = require("robohydra").heads.RoboHydraHeadFilesystem;
exports.getBodyParts = function(conf) {
return {
heads: [
new RoboHydraHeadFilesystem({
mountPath: '/assets/',
documentRoot: '../assets'
})
]
};
};

View File

@ -0,0 +1,156 @@
var roboHydraHeadDAV = require("../headdav");
exports.getBodyParts = function(conf) {
return {
heads: [
/* base URL */
new RoboHydraHeadDAV({
path: "/dav/",
handler: function(req,res,next) { }
}),
/* principal URL */
new RoboHydraHeadDAV({
path: "/dav/principals/users/test",
handler: function(req,res,next) {
if (req.method == "PROPFIND" && req.rawBody.toString().match(/home-set/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>' + req.url + '</href> \
<propstat>\
<prop>\
<CARD:addressbook-home-set xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
<href>/dav/addressbooks/test/</href>\
</CARD:addressbook-home-set>\
<CAL:calendar-home-set xmlns:CAL="urn:ietf:params:xml:ns:caldav">\
<href>/dav/calendars/test/</href>\
</CAL:calendar-home-set>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
}),
/* address-book home set */
new RoboHydraHeadDAV({
path: "/dav/addressbooks/test",
handler: function(req,res,next) {
if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>/dav/addressbooks/test/useless-member</href>\
<propstat>\
<prop>\
<resourcetype/>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
<response>\
<href>/dav/addressbooks/test/default.vcf/</href>\
<propstat>\
<prop xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
<resourcetype>\
<collection/>\
<CARD:addressbook/>\
</resourcetype>\
<CARD:addressbook-description>\
Default Address Book\
</CARD:addressbook-description>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
}),
/* calendar home set */
new RoboHydraHeadDAV({
path: "/dav/calendars/test",
handler: function(req,res,next) {
if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav">\
<response>\
<href>/dav/calendars/test/shared.forbidden</href>\
<propstat>\
<prop>\
<resourcetype/>\
</prop>\
<status>HTTP/1.1 403 Forbidden</status>\
</propstat>\
</response>\
<response>\
<href>/dav/calendars/test/private.ics</href>\
<propstat>\
<prop>\
<resourcetype>\
<collection/>\
<CAL:calendar/>\
</resourcetype>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
<response>\
<href>/dav/calendars/test/work.ics</href>\
<propstat>\
<prop>\
<resourcetype>\
<collection/>\
<CAL:calendar/>\
</resourcetype>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
}),
/* non-existing file */
new RoboHydraHeadDAV({
path: "/dav/collection/new.file",
handler: function(req,res,next) {
if (req.method == "PUT") {
if (req.headers['if-match']) /* can't overwrite new file */
res.statusCode = 412;
else
res.statusCode = 201;
} else if (req.method == "DELETE")
res.statusCode = 404;
}
}),
/* existing file */
new RoboHydraHeadDAV({
path: "/dav/collection/existing.file",
handler: function(req,res,next) {
if (req.method == "PUT") {
if (req.headers['if-none-match']) /* requested "don't overwrite", but this file exists */
res.statusCode = 412;
else
res.statusCode = 204;
} else if (req.method == "DELETE")
res.statusCode = 204;
}
})
]
};
};

View File

@ -0,0 +1,36 @@
var roboHydraHeadDAV = require("../headdav");
exports.getBodyParts = function(conf) {
return {
heads: [
/* address-book home set */
new RoboHydraHeadDAV({
path: "/dav-invalid/addressbooks/user%40domain/",
handler: function(req,res,next) {
if (req.method == "PROPFIND" && req.rawBody.toString().match(/addressbook-description/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>/dav/addressbooks/user@domain/My Contacts.vcf/</href>\
<propstat>\
<prop xmlns:CARD="urn:ietf:params:xml:ns:carddav">\
<resourcetype>\
<collection/>\
<CARD:addressbook/>\
</resourcetype>\
<CARD:addressbook-description>\
Address Book with @ and space in URL\
</CARD:addressbook-description>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
}
}
})
]
};
};

View File

@ -0,0 +1,50 @@
var roboHydra = require("robohydra"),
roboHydraHeads = roboHydra.heads,
roboHydraHead = roboHydraHeads.RoboHydraHead;
RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({
name: 'WebDAV Server',
mandatoryProperties: [ 'path', 'handler' ],
parentPropBuilder: function() {
var myHandler = this.handler;
return {
path: this.path,
handler: function(req,res,next) {
// default DAV behavior
res.headers['DAV'] = 'addressbook, calendar-access';
res.statusCode = 500;
// DAV operations that work on all URLs
if (req.method == "OPTIONS") {
res.statusCode = 204;
res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE';
} else if (req.method == "PROPFIND" && req.rawBody.toString().match(/current-user-principal/)) {
res.statusCode = 207;
res.write('\<?xml version="1.0" encoding="utf-8" ?>\
<multistatus xmlns="DAV:">\
<response>\
<href>' + req.url + '</href> \
<propstat>\
<prop>\
<current-user-principal>\
<href>/dav/principals/users/test</href>\
</current-user-principal>\
</prop>\
<status>HTTP/1.1 200 OK</status>\
</propstat>\
</response>\
</multistatus>\
');
} else
myHandler(req,res,next);
res.end();
}
}
}
});
module.exports = RoboHydraHeadDAV;

View File

@ -0,0 +1,18 @@
var RoboHydraHead = require("robohydra").heads.RoboHydraHead;
exports.getBodyParts = function(conf) {
return {
heads: [
new RoboHydraHead({
path: "/redirect",
handler: function(req,res,next) {
res.statusCode = 302;
res.headers = {
location: 'http://www.example.com'
}
res.end();
}
})
]
};
};

2
test/robohydra/run.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
node_modules/robohydra/bin/robohydra.js davdroid.conf -I plugins

View File

@ -0,0 +1,39 @@
package at.bitfire.davdroid.test;
import java.net.URI;
import java.net.URISyntaxException;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.URIUtils;
public class URIUtilsTest extends InstrumentationTestCase {
final static String
ROOT_URI = "http://server/",
BASE_URI = ROOT_URI + "dir/";
URI baseURI;
@Override
protected void setUp() throws Exception {
baseURI = new URI(BASE_URI);
}
public void testIsSame() throws URISyntaxException {
assertTrue(URIUtils.isSame(new URI(ROOT_URI + "my@email/"), new URI(ROOT_URI + "my%40email/")));
}
public void testResolve() {
// resolve absolute URL
assertEquals(ROOT_URI + "file", URIUtils.resolve(baseURI, "/file").toString());
// resolve relative URL (default case)
assertEquals(BASE_URI + "file", URIUtils.resolve(baseURI, "file").toString());
// resolve relative URL with special characters
assertEquals(BASE_URI + "fi:le", URIUtils.resolve(baseURI, "fi:le").toString());
assertEquals(BASE_URI + "fi@le", URIUtils.resolve(baseURI, "fi@le").toString());
// resolve URL with other schema
assertEquals("https://server", URIUtils.resolve(baseURI, "https://server").toString());
}
}

View File

@ -1,42 +0,0 @@
package at.bitfire.davdroid.webdav.test;
import java.io.IOException;
import java.net.URI;
import org.apache.http.HttpException;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.InvalidDavResponseException;
import at.bitfire.davdroid.webdav.WebDavCollection;
import at.bitfire.davdroid.webdav.WebDavResource;
public class WebDavCollectionTest extends InstrumentationTestCase {
WebDavCollection dav;
protected void setUp() throws Exception {
dav = new WebDavCollection(new URI("https://wurd.dev001.net/radicale/test/"), "test", "test", true);
}
public void testAutoDetection() throws InvalidDavResponseException, IOException, HttpException {
WebDavCollection myDav = dav;
myDav.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
String currentUserPrincipal = myDav.getCurrentUserPrincipal();
assertEquals("/radicale/test/", currentUserPrincipal);
myDav = new WebDavCollection(dav, currentUserPrincipal);
myDav.propfind(HttpPropfind.Mode.HOME_SETS);
String homeSet = myDav.getAddressbookHomeSet();
assertEquals("/radicale/test/", homeSet);
assertEquals("/radicale/test/", myDav.getCalendarHomeSet());
myDav = new WebDavCollection(dav, homeSet);
myDav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS);
assertEquals(3, myDav.getMembers().size());
for (WebDavResource member : myDav.getMembers())
assertTrue(member.isAddressBook() || member.isCalendar());
}
}

View File

@ -1,66 +1,178 @@
package at.bitfire.davdroid.webdav.test;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpException;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.NotFoundException;
import at.bitfire.davdroid.webdav.PreconditionFailedException;
import at.bitfire.davdroid.webdav.WebDavResource;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
// tests require running robohydra!
public class WebDavResourceTest extends InstrumentationTestCase {
URI davBaseURI, uriWithoutDAV, uriWithRedirection;
final static String davUsername = "test", davPassword = "test";
static final String ROBOHYDRA_BASE = "http://10.0.0.119:3000/";
static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
AssetManager assetMgr;
WebDavResource simpleFile,
davCollection, davNonExistingFile, davExistingFile;
@Override
protected void setUp() throws Exception {
davBaseURI = new URI("https://wurd.dev001.net/radicale/test/");
uriWithoutDAV = new URI("http://www.apache.org");
uriWithRedirection = new URI("http://wurd.dev001.net/public/");
assetMgr = getInstrumentation().getContext().getResources().getAssets();
simpleFile = new WebDavResource(new URI(ROBOHYDRA_BASE + "assets/test.random"), false);
davCollection = new WebDavResource(new URI(ROBOHYDRA_BASE + "dav"), true);
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
}
/* test resource name handling */
public void testGet() throws URISyntaxException, IOException, HttpException {
WebDavResource dav = new WebDavResource(uriWithoutDAV, "", "", false, true);
dav.get();
InputStream is = dav.getContent();
assertNotNull(is);
public void testGetName() {
// collection names should have a trailing slash
assertEquals("dav", davCollection.getName());
// but non-collection names shouldn't
assertEquals("test.random", simpleFile.getName());
}
public void testTrailingSlash() throws URISyntaxException {
WebDavResource dav = new WebDavResource(new URI("http://server/path"), "", "", false, true);
assertEquals("/path/", dav.getLocation().getPath());
}
// collections should have a trailing slash
assertEquals("/dav/", davCollection.getLocation().getPath());
// but non-collection members shouldn't
assertEquals("/assets/test.random", simpleFile.getLocation().getPath());
}
/* test feature detection */
public void testOptions() throws URISyntaxException, IOException, HttpException {
String[] davMethods = new String[] { "PROPFIND", "PUT", "DELETE" },
String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE" },
davCapabilities = new String[] { "addressbook", "calendar-access" };
// server without DAV
WebDavResource dav = new WebDavResource(uriWithoutDAV, "", "", false, true);
dav.options();
simpleFile.options();
for (String method : davMethods)
assertFalse(dav.supportsMethod(method));
assertFalse(simpleFile.supportsMethod(method));
for (String capability : davCapabilities)
assertFalse(dav.supportsDAV(capability));
assertFalse(simpleFile.supportsDAV(capability));
// server with DAV
dav = new WebDavResource(davBaseURI, davUsername, davPassword, false, true);
dav.options();
davCollection.options();
for (String davMethod : davMethods)
assert(dav.supportsMethod(davMethod));
assert(davCollection.supportsMethod(davMethod));
for (String capability : davCapabilities)
assert(dav.supportsDAV(capability));
assert(davCollection.supportsDAV(capability));
}
public void testRedirections() throws URISyntaxException, IOException {
WebDavResource dav = new WebDavResource(uriWithRedirection, "", "", false, true);
public void testPropfindCurrentUserPrincipal() throws IOException, HttpException {
assertTrue(davCollection.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL));
assertEquals("/dav/principals/users/test", davCollection.getCurrentUserPrincipal());
assertFalse(simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL));
assertNull(simpleFile.getCurrentUserPrincipal());
}
public void testPropfindHomeSets() throws IOException, HttpException {
WebDavResource dav = new WebDavResource(davCollection, "principals/users/test");
dav.propfind(HttpPropfind.Mode.HOME_SETS);
assertEquals("/dav/addressbooks/test/", dav.getAddressbookHomeSet());
assertEquals("/dav/calendars/test/", dav.getCalendarHomeSet());
}
public void testPropfindAddressBooks() throws IOException, HttpException {
WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test");
dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS);
assertEquals(2, dav.getMembers().size());
for (WebDavResource member : dav.getMembers()) {
if (member.getName().equals("default.vcf"))
assertTrue(member.isAddressBook());
else
assertFalse(member.isAddressBook());
assertFalse(member.isCalendar());
}
}
public void testPropfindCalendars() throws IOException, HttpException {
WebDavResource dav = new WebDavResource(davCollection, "calendars/test");
dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS);
assertEquals(3, dav.getMembers().size());
for (WebDavResource member : dav.getMembers()) {
if (member.getName().contains(".ics"))
assertTrue(member.isCalendar());
else
assertFalse(member.isCalendar());
assertFalse(member.isAddressBook());
}
}
/* test normal HTTP */
public void testDontFollowRedirections() throws URISyntaxException, IOException {
WebDavResource redirection = new WebDavResource(new URI(ROBOHYDRA_BASE + "redirect"), false);
try {
dav.options();
redirection.get();
} catch (HttpException e) {
return;
}
fail();
}
public void testGet() throws URISyntaxException, IOException, HttpException {
simpleFile.get();
assertTrue(IOUtils.contentEquals(
assetMgr.open("test.random", AssetManager.ACCESS_STREAMING),
simpleFile.getContent()
));
}
public void testPutAddDontOverwrite() throws IOException, HttpException {
// should succeed on a non-existing file
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE);
// should fail on an existing file
try {
davExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE);
} catch(PreconditionFailedException ex) {
return;
}
fail();
}
public void testPutUpdateDontOverwrite() throws IOException, HttpException {
// should succeed on an existing file
davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
// should fail on a non-existing file
try {
davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE);
} catch(PreconditionFailedException ex) {
return;
}
fail();
}
public void testDelete() throws IOException, HttpException {
// should succeed on an existing file
davExistingFile.delete();
// should fail on a non-existing file
try {
davNonExistingFile.delete();
} catch (NotFoundException e) {
return;
}
fail();
}
}