Commit a32ec6d9 authored by Ricki Hirner's avatar Ricki Hirner

Use callback/streaming approach instead of bad-scaling lists

parent fc6cf159
Pipeline #25515556 passed with stages
in 3 minutes and 36 seconds
......@@ -19,6 +19,9 @@ dav4android is licensed under [Mozilla Public License, v. 2.0](LICENSE).
For questions, suggestions etc. please use the DAVdroid forum:
https://www.davdroid.com/forums/
If you want to contribute, please work in your own repository and then
notify us on your changes so that we can backport them.
Email: [play@bitfire.at](mailto:play@bitfire.at)
......
buildscript {
ext.kotlin_version = '1.2.50'
ext.kotlin_version = '1.2.51'
ext.dokka_version = '0.9.16'
repositories {
......
......@@ -7,6 +7,7 @@
package at.bitfire.dav4android
import okhttp3.*
import okhttp3.Response
import okio.Buffer
import okio.ByteString
import java.io.IOException
......@@ -23,7 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger
* Authentication methods/credentials found to be working will be cached for further requests
* (this is why the interceptor is needed).
*
* Usage: Set as authenticator **and** as network interceptor.
* Usage: Set as authenticator *and* as network interceptor.
*/
class BasicDigestAuthHandler(
/** Authenticate only against hosts ending with this domain (may be null, which means no restriction) */
......@@ -54,8 +55,8 @@ class BasicDigestAuthHandler(
}
// cached authentication schemes
var basicAuth: HttpUtils.AuthScheme? = null
var digestAuth: HttpUtils.AuthScheme? = null
private var basicAuth: HttpUtils.AuthScheme? = null
private var digestAuth: HttpUtils.AuthScheme? = null
fun authenticateRequest(request: Request, response: Response?): Request? {
......
......@@ -24,14 +24,19 @@ class DavAddressBook @JvmOverloads constructor(
val MIME_VCARD4 = MediaType.parse("text/vcard;version=4.0")
}
/**
* Sends an addressbook-query REPORT request to the resource.
*
* @param callback called for every WebDAV response XML element in the result
*
* @return list of properties which have been received in the Multi-Status response, but
* are not part of response XML elements
*
* @throws IOException on I/O error
* @throws HttpException on HTTP error
* @throws DavException on DAV error
* @throws DavException on WebDAV error
*/
fun addressbookQuery(): DavResponse {
fun addressbookQuery(callback: DavResponseCallback): List<Property> {
/* <!ELEMENT addressbook-query ((DAV:allprop |
DAV:propname |
DAV:prop)?, filter, limit?)>
......@@ -53,25 +58,32 @@ class DavAddressBook @JvmOverloads constructor(
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook-query")
serializer.endDocument()
val response = httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "1")
.build()).execute()
checkStatus(response)
assertMultiStatus(response)
return processMultiStatus(response.body()?.charStream()!!)
followRedirects {
httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "1")
.build()).execute()
}.use { response ->
return processMultiStatus(response, callback)
}
}
/**
* Sends an addressbook-multiget REPORT request to the resource.
*
* @param urls list of vCard URLs to be requested
* @param vCard4 whether vCards should be requested as vCard4 4.0 (true: 4.0, false: 3.0)
* @param callback called for every WebDAV response XML element in the result
*
* @return list of properties which have been received in the Multi-Status response, but
* are not part of response XML elements
*
* @throws IOException on I/O error
* @throws HttpException on HTTP error
* @throws DavException on DAV error
* @throws DavException on WebDAV error
*/
fun multiget(urls: List<HttpUrl>, vCard4: Boolean): DavResponse {
fun multiget(urls: List<HttpUrl>, vCard4: Boolean, callback: DavResponseCallback): List<Property> {
/* <!ELEMENT addressbook-multiget ((DAV:allprop |
DAV:propname |
DAV:prop)?,
......@@ -104,16 +116,15 @@ class DavAddressBook @JvmOverloads constructor(
serializer.endTag(XmlUtils.NS_CARDDAV, "addressbook-multiget")
serializer.endDocument()
val response = httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "0") // "The request MUST include a Depth: 0 header [...]"
.build()).execute()
checkStatus(response)
assertMultiStatus(response)
return processMultiStatus(response.body()?.charStream()!!)
followRedirects {
httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "0") // "The request MUST include a Depth: 0 header [...]"
.build()).execute()
}.use {
return processMultiStatus(it, callback)
}
}
}
......@@ -34,11 +34,20 @@ class DavCalendar @JvmOverloads constructor(
/**
* Sends a calendar-query REPORT to the resource.
*
* @param component requested component name (like VEVENT or VTODO)
* @param start time-range filter: start date (optional)
* @param end time-range filter: end date (optional)
* @param callback called for every WebDAV response XML element in the result
*
* @return list of properties which have been received in the Multi-Status response, but
* are not part of response XML elements
*
* @throws IOException on I/O error
* @throws HttpException on HTTP error
* @throws DavException on DAV error
* @throws DavException on WebDAV error
*/
fun calendarQuery(component: String, start: Date?, end: Date?): DavResponse {
fun calendarQuery(component: String, start: Date?, end: Date?, callback: DavResponseCallback): List<Property> {
/* <!ELEMENT calendar-query ((DAV:allprop |
DAV:propname |
DAV:prop)?, filter, timezone?)>
......@@ -80,25 +89,31 @@ class DavCalendar @JvmOverloads constructor(
serializer.endTag(XmlUtils.NS_CALDAV, "calendar-query")
serializer.endDocument()
val response = httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "1")
.build()).execute()
checkStatus(response)
assertMultiStatus(response)
return processMultiStatus(response.body()?.charStream()!!)
followRedirects {
httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "1")
.build()).execute()
}.use {
return processMultiStatus(it, callback)
}
}
/**
* Sends a calendar-multiget REPORT to the resource.
*
* @param urls list of iCalendar URLs to be requested
* @param callback called for every WebDAV response XML element in the result
*
* @return list of properties which have been received in the Multi-Status response, but
* are not part of response XML elements
*
* @throws IOException on I/O error
* @throws HttpException on HTTP error
* @throws DavException on DAV error
* @throws DavException on WebDAV error
*/
fun multiget(urls: List<HttpUrl>): DavResponse {
fun multiget(urls: List<HttpUrl>, callback: DavResponseCallback): List<Property> {
/* <!ELEMENT calendar-multiget ((DAV:allprop |
DAV:propname |
DAV:prop)?, DAV:href+)>
......@@ -126,15 +141,14 @@ class DavCalendar @JvmOverloads constructor(
serializer.endTag(XmlUtils.NS_CALDAV, "calendar-multiget")
serializer.endDocument()
val response = httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.build()).execute()
checkStatus(response)
assertMultiStatus(response)
return processMultiStatus(response.body()?.charStream()!!)
followRedirects {
httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.build()).execute()
}.use {
return processMultiStatus(it, callback)
}
}
}
......@@ -16,6 +16,9 @@ import okhttp3.RequestBody
import java.io.StringWriter
import java.util.logging.Logger
/**
* Represents a WebDAV collection.
*/
open class DavCollection @JvmOverloads constructor(
httpClient: OkHttpClient,
location: HttpUrl,
......@@ -23,19 +26,22 @@ open class DavCollection @JvmOverloads constructor(
): DavResource(httpClient, location, log) {
/**
* Sends a REPORT sync-collection request. If a sync-token is returned, it will be made
* available in [properties].
* Sends a REPORT sync-collection request.
*
* @param syncToken sync-token to be sent with the request
* @param infiniteDepth sync-level to be sent with the request: false = "1", true = "infinite"
* @param limit maximum number of results (may cause truncation)
* @param properties WebDAV properties to be requested
* @param callback called for every WebDAV response XML element in the result
*
* @return list of properties which have been received in the Multi-Status response, but
* are not part of response XML elements (like `sync-token` which is returned as [SyncToken])
*
* @throws java.io.IOException on I/O error
* @throws HttpException on HTTP error
* @throws DavException on DAV error
* @throws DavException on WebDAV error
*/
fun reportChanges(syncToken: String?, infiniteDepth: Boolean, limit: Int?, vararg properties: Property.Name): DavResponse {
fun reportChanges(syncToken: String?, infiniteDepth: Boolean, limit: Int?, vararg properties: Property.Name, callback: DavResponseCallback): List<Property> {
/* <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
<!ELEMENT sync-token CDATA> <!-- Text MUST be a URI -->
......@@ -74,16 +80,15 @@ open class DavCollection @JvmOverloads constructor(
serializer.endTag(XmlUtils.NS_WEBDAV, "sync-collection")
serializer.endDocument()
val response = httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "0")
.build()).execute()
checkStatus(response)
assertMultiStatus(response)
return processMultiStatus(response.body()?.charStream()!!)
followRedirects {
httpClient.newCall(Request.Builder()
.url(location)
.method("REPORT", RequestBody.create(MIME_XML, writer.toString()))
.header("Depth", "0")
.build()).execute()
}.use {
return processMultiStatus(it, callback)
}
}
}
\ No newline at end of file
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package at.bitfire.dav4android
import at.bitfire.dav4android.property.SyncToken
import okhttp3.HttpUrl
import okhttp3.Response
import okhttp3.ResponseBody
import java.io.Closeable
/**
* Immutable container for a WebDAV multistatus response. Note that property elements
* are not immutable automatically.
*/
class DavResponse private constructor(
/** resource this response is about */
val url: HttpUrl,
/** corresponding HTTP response object */
val body: ResponseBody?,
/** HTTP capabilities reported by an OPTIONS response */
val capabilities: Set<String>,
/** WebDAV properties of this resource */
val properties: List<Property>,
/** members of the requested collection */
val members: List<DavResponse>,
/** removed members of the requested collection (HTTP status 404) */
val removedMembers: List<DavResponse>,
/** information about resources which are not members of the requested collection */
val related: List<DavResponse>,
/** sync-token as returned by REPORT sync-collection */
val syncToken: SyncToken?,
/** whether further results are available (requested collection had HTTP status 507) */
val furtherResults: Boolean
): Closeable {
/**
* After closing this response, the [body] will not be usable anymore, but other properties
* can be used normally.
*/
override fun close() {
body?.close()
}
/**
* Gets the file name of this resource. See [HttpUtils.fileName] for details.
*/
fun fileName() = HttpUtils.fileName(url)
/**
* Convenience method to get a certain property from the current response. Does't take
* members or related resources into consideration.
*/
operator fun<T: Property> get(clazz: Class<T>): T? {
return properties.filterIsInstance(clazz).firstOrNull()
}
/**
* Recursively searches for a property, i.e. in this object and in
* [members] and [related] and returns the first occurrence with its source.
*/
fun<T: Property> searchProperty(clazz: Class<T>): Pair<DavResponse, T>? {
get(clazz)?.let { return Pair(this, it) }
members.forEach { response ->
response[clazz]?.let { return Pair(response, it) }
}
related.forEach { response ->
response[clazz]?.let { return Pair(response, it) }
}
return null
}
/**
* Recursively (i.e. in this object, and in [members] and [related]) searches for a
* property, and returns all occurrences together with their sources.
*/
fun<T: Property> searchProperties(clazz: Class<T>): Map<DavResponse, T> {
val map = mutableMapOf<DavResponse, T>()
get(clazz)?.let { map[this] = it }
members.forEach { response ->
response[clazz]?.let { map[response] = it }
}
related.forEach { response ->
response[clazz]?.let { map[response] = it }
}
return map
}
class Builder(
val url: HttpUrl
) {
private var responseBody: ResponseBody? = null
fun responseBody(newValue: ResponseBody?): Builder {
responseBody = newValue
return this
}
private var capabilities: Set<String> = setOf()
fun capabilities(newValue: Set<String>): Builder {
capabilities = newValue
return this
}
private var properties: List<Property> = listOf()
fun properties(newValue: List<Property>): Builder {
properties = newValue
return this
}
private val members: MutableList<DavResponse.Builder> = mutableListOf()
fun addMember(member: DavResponse.Builder): Builder {
members += member
return this
}
private val removedMembers: MutableList<DavResponse.Builder> = mutableListOf()
fun addRemovedMember(member: DavResponse.Builder): Builder {
removedMembers += member
return this
}
private val related: MutableList<DavResponse.Builder> = mutableListOf()
fun addRelated(related: DavResponse.Builder): Builder {
this.related += related
return this
}
private var syncToken: SyncToken? = null
fun syncToken(newValue: SyncToken?): Builder {
syncToken = newValue
return this
}
private var furtherResults = false
fun furtherResults(newValue: Boolean): Builder {
furtherResults = newValue
return this
}
fun build(): DavResponse = DavResponse(
url,
responseBody,
capabilities,
properties,
members.map { it.build() },
removedMembers.map { it.build() },
related.map { it.build() },
syncToken,
furtherResults
)
}
}
/*
* Copyright © 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
import org.xmlpull.v1.XmlPullParser
/**
* Represents an XML precondition/postcondition error. Every error has a name, which is the XML element
* name. Subclassed errors may have more specific information available.
*
* At the moment, there is no logic for subclassing errors.
*/
class Error(
val name: Property.Name
) {
companion object {
fun parseError(parser: XmlPullParser): List<Error> {
val names = mutableSetOf<Property.Name>()
val depth = parser.depth
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
names += Property.Name(parser.namespace, parser.name)
eventType = parser.next()
}
return names.map { Error(it) }
}
}
}
/*
* Copyright © 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
import at.bitfire.dav4android.Constants.log
import okhttp3.Protocol
import okhttp3.internal.http.StatusLine
import org.xmlpull.v1.XmlPullParser
import java.net.ProtocolException
import java.util.*
/**
* Represents a WebDAV propstat XML element.
*
* <!ELEMENT propstat (prop, status, error?, responsedescription?) >
*/
data class PropStat(
val properties: List<Property>,
val status: StatusLine,
val error: List<Error>? = null
) {
fun isSuccess() = status.code/100 == 2
companion object {
private val ASSUMING_OK = StatusLine(Protocol.HTTP_1_1, 200, "Assuming OK")
private val INVALID_STATUS = StatusLine(Protocol.HTTP_1_1, 500, "Invalid status line")
fun parse(parser: XmlPullParser): PropStat {
val depth = parser.depth
var status: StatusLine? = null
val prop = LinkedList<Property>()
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
if (parser.namespace == XmlUtils.NS_WEBDAV)
when (parser.name) {
"prop" ->
prop.addAll(Property.parse(parser))
"status" ->
status = try {
StatusLine.parse(parser.nextText())
} catch (e: ProtocolException) {
// invalid status line, treat as 500 Internal Server Error
INVALID_STATUS
}
}
eventType = parser.next()
}
return PropStat(prop, status ?: ASSUMING_OK)
}
}
}
\ No newline at end of file
......@@ -6,12 +6,15 @@
package at.bitfire.dav4android
import at.bitfire.dav4android.Constants.log
import org.xmlpull.v1.XmlPullParser
import java.io.Serializable
import java.util.*
/**
* A WebDAV property.
* Represents a WebDAV property.
*
* Every [Property] must define a static field (use @JvmStatic) called NAME of type [Property.Name],
* Every [Property] must define a static field (use `@JvmStatic`) called `NAME` of type [Property.Name],
* which will be accessed by reflection.
*/
interface Property {
......@@ -33,4 +36,32 @@ interface Property {
override fun toString() = "$namespace$name"
}
companion object {
fun parse(parser: XmlPullParser): List<Property> {
// <!ELEMENT prop ANY >
val depth = parser.depth
val properties = LinkedList<Property>()
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1) {
val depthBeforeParsing = parser.depth
val name = Property.Name(parser.namespace, parser.name)
val property = PropertyRegistry.create(name, parser)
assert(parser.depth == depthBeforeParsing)
if (property != null) {
properties.add(property)
} else
log.fine("Ignoring unknown property $name")
}
eventType = parser.next()
}
return properties
}
}
}
/*
* Copyright © 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
import at.bitfire.dav4android.Constants.log
import at.bitfire.dav4android.property.ResourceType
import okhttp3.HttpUrl
import okhttp3.Protocol
import okhttp3.internal.http.StatusLine
import org.xmlpull.v1.XmlPullParser
import java.net.ProtocolException
/**
* Represents a WebDAV response XML Element.
*
* <!ELEMENT response (href, ((href*, status)|(propstat+)),
* error?, responsedescription? , location?) >
*/
data class Response(
/**
* URL of the requested resource. For instance, if `this` is a result
* of a PROPFIND request, the `requestedUrl` would be the URL where the
* PROPFIND request has been sent to (usually the collection URL).
*/
val requestedUrl: HttpUrl,
/**
* URL of this response (`href` element)
*/
val href: HttpUrl,
/**
* status of this response (`status` XML element)
*/
val status: StatusLine?,
/**
* property/status elements (`propstat` XML elements)
*/
val propstat: List<PropStat>,
/**
* list of precondition/postcondition elements (`error` XML elements)
*/
val error: List<Error>? = null,
/**
* new location of this response (`location` XML element), used for redirects
*/
val newLocation: HttpUrl? = null
) {
enum class HrefRelation {
SELF, MEMBER, OTHER
}
/**
* All properties from propstat elements with empty status or status code 2xx.
*/
val properties: List<Property> by lazy {
if (isSuccess())
propstat.filter { it.isSuccess() }.map { it.properties }.flatten()
else
emptyList()
}
/**
* Convenience method to get a certain property with empty status or status code 2xx