Commit 91876d3f authored by Ricki Hirner's avatar Ricki Hirner

PROPFIND improvements

* handle redirects
* new properties
parent c8dffb7f
package at.bitfire.dav4android;
import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import junit.framework.TestCase;
import java.io.IOException;
import java.net.Proxy;
import java.nio.charset.Charset;
import java.util.Collection;
import at.bitfire.dav4android.exception.DavException;
import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.exception.InvalidDavResponseException;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.ResourceType;
......@@ -259,7 +251,7 @@ public class DavResourceTest extends TestCase {
boolean ok[] = new boolean[4];
for (DavResource member : dav.members) {
if (url.resolve("/dav/subcollection/").equals(member.location)) {
assertTrue(((ResourceType) member.properties.get(ResourceType.NAME)).types.contains(ResourceType.WEBDAV_COLLECTION));
assertTrue(((ResourceType) member.properties.get(ResourceType.NAME)).types.contains(ResourceType.COLLECTION));
assertEquals("A Subfolder", ((DisplayName) member.properties.get(DisplayName.NAME)).displayName);
ok[0] = true;
} else if (url.resolve("/dav/uid@host:file").equals(member.location)) {
......
......@@ -4,7 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<application android:allowBackup="true"
android:label="DAV4Android">
android:label="@string/app_name">
</application>
......
......@@ -10,6 +10,7 @@ package at.bitfire.dav4android;
import android.util.Log;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
......@@ -27,7 +28,6 @@ import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.net.ProtocolException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
......@@ -37,21 +37,24 @@ import at.bitfire.dav4android.exception.HttpException;
import at.bitfire.dav4android.exception.InvalidDavResponseException;
import at.bitfire.dav4android.exception.UnsupportedDavException;
import at.bitfire.dav4android.property.ResourceType;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class DavResource {
public final MediaType MEDIA_TYPE_XML = MediaType.parse("application/xml; charset=utf-8");
protected final OkHttpClient httpClient;
final HttpUrl location;
final PropertyCollection properties = new PropertyCollection();
final Set<DavResource> members = new HashSet<>();
public HttpUrl location;
public final PropertyCollection properties = new PropertyCollection();
public final Set<DavResource> members = new HashSet<>();
static private PropertyRegistry registry = PropertyRegistry.DEFAULT;
public DavResource(OkHttpClient httpClient, HttpUrl location) {
this.httpClient = httpClient;
this.location = location;
}
public void propfind(int depth, Property.Name... reqProp) throws IOException, HttpException, DavException {
// build XML request body
......@@ -69,11 +72,31 @@ public class DavResource {
serializer.endTag(XmlUtils.NS_WEBDAV, "propfind");
serializer.endDocument();
Response response = httpClient.newCall(new Request.Builder()
.url(location)
.method("PROPFIND", RequestBody.create(MEDIA_TYPE_XML, writer.toString()))
.header("Depth", String.valueOf(depth))
.build()).execute();
// redirects must not followed automatically (as it may rewrite PROPFIND requests to GET requests)
httpClient.setFollowRedirects(false);
Response response = null;
for (int attempt = 0; attempt < 3; attempt++) {
Constants.log.info("Attempt " + attempt);
response = httpClient.newCall(new Request.Builder()
.url(location)
.method("PROPFIND", RequestBody.create(MEDIA_TYPE_XML, writer.toString()))
.header("Depth", String.valueOf(depth))
.header("Authorization", Credentials.basic("XXXXXXXXXXXX", "XXXXXXXXx")) // TODO
.build()).execute();
if (response.code()/100 == 3) {
String href = response.header("Location");
if (href != null) {
HttpUrl newLocation = location.resolve(href);
if (newLocation != null)
location = newLocation;
else
throw new HttpException(500, "Redirect without Location");
}
} else
break;
}
checkStatus(response);
......@@ -85,8 +108,8 @@ public class DavResource {
MediaType mediaType = response.body().contentType();
if (mediaType != null) {
String type = mediaType.type() + "/" + mediaType.subtype();
if (!"application/xml".equals(type) || "text/xml".equals(type))
if (!("application".equals(mediaType.type()) || "text".equals(mediaType.type())) ||
!"xml".equals(mediaType.subtype()))
throw new InvalidDavResponseException("Received non-XML 207 Multi-Status");
} else
Constants.log.warn("Received multi-status response without Content-Type, assuming XML");
......@@ -222,7 +245,7 @@ public class DavResource {
// if we know this resource is a collection, make sure href has a trailing slash (for clarity and resolving relative paths)
ResourceType type = (ResourceType)properties.get(ResourceType.NAME);
if (type != null && type.types.contains(ResourceType.WEBDAV_COLLECTION))
if (type != null && type.types.contains(ResourceType.COLLECTION))
href = UrlUtils.withTrailingSlash(href);
Constants.log.debug("Received <response> for " + href + ", status: " + status + ", properties: " + properties);
......
......@@ -5,7 +5,12 @@ import org.xmlpull.v1.XmlPullParser;
import java.util.HashMap;
import java.util.Map;
import at.bitfire.dav4android.property.AddressbookDescription;
import at.bitfire.dav4android.property.AddressbookHomeSet;
import at.bitfire.dav4android.property.CalendarColor;
import at.bitfire.dav4android.property.CalendarDescription;
import at.bitfire.dav4android.property.CalendarHomeSet;
import at.bitfire.dav4android.property.CurrentUserPrincipal;
import at.bitfire.dav4android.property.DisplayName;
import at.bitfire.dav4android.property.ResourceType;
import at.bitfire.dav4android.property.SupportedAddressData;
......@@ -18,11 +23,16 @@ public class PropertyRegistry {
static {
DEFAULT.register(new ResourceType.Factory());
DEFAULT.register(new DisplayName.Factory());
DEFAULT.register(new CurrentUserPrincipal.Factory());
// CardDAV
DEFAULT.register(new AddressbookHomeSet.Factory());
DEFAULT.register(new AddressbookDescription.Factory());
DEFAULT.register(new SupportedAddressData.Factory());
// CalDAV
DEFAULT.register(new CalendarHomeSet.Factory());
DEFAULT.register(new CalendarDescription.Factory());
DEFAULT.register(new CalendarColor.Factory());
}
......
......@@ -11,6 +11,7 @@ public class XmlUtils {
public static final String
NS_WEBDAV = "DAV:",
NS_CALDAV = "urn:ietf:params:xml:ns:caldav",
NS_CARDDAV = "urn:ietf:params:xml:ns:carddav",
NS_APPLE_ICAL = "http://apple.com/ns/ical/";
......
/*
* Copyright © 2013 – 2015 Ricki 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.dav4android.property;
import android.text.TextUtils;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.PropertyFactory;
import at.bitfire.dav4android.XmlUtils;
import lombok.ToString;
@ToString
public class AddressbookDescription implements Property {
public static final Property.Name NAME = new Name(XmlUtils.NS_CARDDAV, "addressbook-description");
public String description;
public static class Factory implements PropertyFactory {
@Override
public Name getName() {
return NAME;
}
@Override
public AddressbookDescription create(XmlPullParser parser) {
AddressbookDescription description = new AddressbookDescription();
try {
// <!ELEMENT addressbook-description (#PCDATA)>
final int depth = parser.getDepth();
int eventType = parser.getEventType();
while (!(eventType == XmlPullParser.END_TAG && parser.getDepth() == depth)) {
if (eventType == XmlPullParser.TEXT && parser.getDepth() == depth)
description.description = parser.getText();
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <addressbook-description>", e);
return null;
}
return description;
}
}
}
/*
* Copyright © 2013 – 2015 Ricki 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.dav4android.property;
import android.text.TextUtils;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.PropertyFactory;
import at.bitfire.dav4android.XmlUtils;
public class AddressbookHomeSet implements Property {
public static final Name NAME = new Name(XmlUtils.NS_CARDDAV, "addressbook-home-set");
public List<String> hrefs = new LinkedList<>();
@Override
public String toString() {
return "hrefs=[" + TextUtils.join(", ", hrefs) + "]";
}
public static class Factory implements PropertyFactory {
@Override
public Name getName() {
return NAME;
}
@Override
public AddressbookHomeSet create(XmlPullParser parser) {
AddressbookHomeSet homeSet = new AddressbookHomeSet();
try {
// <!ELEMENT addressbook-home-set (DAV:href*)>
final int depth = parser.getDepth();
int eventType = parser.getEventType();
while (!(eventType == XmlPullParser.END_TAG && parser.getDepth() == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.getDepth() == depth+1 &&
XmlUtils.NS_WEBDAV.equals(parser.getNamespace()) && "href".equals(parser.getName()))
homeSet.hrefs.add(parser.nextText());
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <addressbook-home-set>", e);
return null;
}
return homeSet;
}
}
}
......@@ -39,7 +39,7 @@ public class CalendarColor implements Property {
calendarColor.color = parser.getText();
eventType = parser.next();
}
} catch(XmlPullParserException |IOException e) {
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <calendar-color>", e);
return null;
}
......
/*
* Copyright © 2013 – 2015 Ricki 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.dav4android.property;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.PropertyFactory;
import at.bitfire.dav4android.XmlUtils;
import lombok.ToString;
@ToString
public class CalendarDescription implements Property {
public static final Name NAME = new Name(XmlUtils.NS_CALDAV, "calendar-description");
public String description;
public static class Factory implements PropertyFactory {
@Override
public Name getName() {
return NAME;
}
@Override
public CalendarDescription create(XmlPullParser parser) {
CalendarDescription description = new CalendarDescription();
try {
// <!ELEMENT calendar-description (#PCDATA)>
final int depth = parser.getDepth();
int eventType = parser.getEventType();
while (!(eventType == XmlPullParser.END_TAG && parser.getDepth() == depth)) {
if (eventType == XmlPullParser.TEXT && parser.getDepth() == depth)
description.description = parser.getText();
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <calendar-description>", e);
return null;
}
return description;
}
}
}
/*
* Copyright © 2013 – 2015 Ricki 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.dav4android.property;
import android.text.TextUtils;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.PropertyFactory;
import at.bitfire.dav4android.XmlUtils;
public class CalendarHomeSet implements Property {
public static final Name NAME = new Name(XmlUtils.NS_CALDAV, "calendar-home-set");
public List<String> hrefs = new LinkedList<>();
@Override
public String toString() {
return "hrefs=[" + TextUtils.join(", ", hrefs) + "]";
}
public static class Factory implements PropertyFactory {
@Override
public Name getName() {
return NAME;
}
@Override
public CalendarHomeSet create(XmlPullParser parser) {
CalendarHomeSet homeSet = new CalendarHomeSet();
try {
// <!ELEMENT calendar-home-set (DAV:href*)>
final int depth = parser.getDepth();
int eventType = parser.getEventType();
while (!(eventType == XmlPullParser.END_TAG && parser.getDepth() == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.getDepth() == depth+1 &&
XmlUtils.NS_WEBDAV.equals(parser.getNamespace()) && "href".equals(parser.getName()))
homeSet.hrefs.add(parser.nextText());
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <calendar-home-set>", e);
return null;
}
return homeSet;
}
}
}
/*
* Copyright © 2013 – 2015 Ricki 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.dav4android.property;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import at.bitfire.dav4android.Constants;
import at.bitfire.dav4android.Property;
import at.bitfire.dav4android.PropertyFactory;
import at.bitfire.dav4android.XmlUtils;
import lombok.ToString;
// see RFC 5397: WebDAV Current Principal Extension
@ToString
public class CurrentUserPrincipal implements Property {
public static final Name NAME = new Name(XmlUtils.NS_WEBDAV, "current-user-principal");
public String href;
public static class Factory implements PropertyFactory {
@Override
public Name getName() {
return NAME;
}
@Override
public CurrentUserPrincipal create(XmlPullParser parser) {
CurrentUserPrincipal principal = new CurrentUserPrincipal();
try {
// <!ELEMENT current-user-principal (unauthenticated | href)>
final int depth = parser.getDepth();
int eventType = parser.getEventType();
while (!(eventType == XmlPullParser.END_TAG && parser.getDepth() == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.getDepth() == depth+1 &&
XmlUtils.NS_WEBDAV.equals(parser.getNamespace()) && "href".equals(parser.getName()))
principal.href = parser.nextText();
eventType = parser.next();
}
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <current-user-principal>", e);
return null;
}
return principal;
}
}
}
......@@ -7,8 +7,6 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import at.bitfire.dav4android.Constants;
......@@ -21,7 +19,10 @@ import lombok.ToString;
public class ResourceType implements Property {
public static final Name NAME = new Name(XmlUtils.NS_WEBDAV, "resourcetype");
public static final Name WEBDAV_COLLECTION = new Name(XmlUtils.NS_WEBDAV, "collection");
public static final Name
COLLECTION = new Name(XmlUtils.NS_WEBDAV, "collection"),
ADDRESSBOOK = new Name(XmlUtils.NS_CARDDAV, "addressbook"),
CALENDAR = new Name(XmlUtils.NS_CALDAV, "calendar");
public final Set<Property.Name> types = new HashSet<>();
......@@ -44,16 +45,20 @@ public class ResourceType implements Property {
if (eventType == XmlPullParser.START_TAG && parser.getDepth() == depth + 1) {
String namespace = parser.getNamespace(), name = parser.getName();
// use pre-defined objects to allow types.contains()
// use static objects to allow types.contains()
Name typeName = new Name(parser.getNamespace(), parser.getName());
if (WEBDAV_COLLECTION.equals(typeName))
typeName = WEBDAV_COLLECTION;
if (COLLECTION.equals(typeName))
typeName = COLLECTION;
else if (ADDRESSBOOK.equals(typeName))
typeName = ADDRESSBOOK;
else if (CALENDAR.equals(typeName))
typeName = CALENDAR;
type.types.add(typeName);
}
eventType = parser.next();
}
} catch(XmlPullParserException |IOException e) {
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <resourcetype>", e);
return null;
}
......
......@@ -49,7 +49,7 @@ public class SupportedAddressData implements Property {
}
eventType = parser.next();
}
} catch(XmlPullParserException |IOException e) {
} catch(XmlPullParserException|IOException e) {
Log.e(Constants.LOG_TAG, "Couldn't parse <resourcetype>", e);
return null;
}
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DAV4Android</string>
</resources>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment