Commit 720c01db authored by Ricki Hirner's avatar Ricki Hirner

Make WebDAV multistatus response objects immutable

* every operation on a resource generates exactly one response object
* no merging of properties anymore
parent 7968c5ee
buildscript {
ext.kotlin_version = '1.2.40'
ext.kotlin_version = '1.2.41'
ext.dokka_version = '0.9.16'
repositories {
......@@ -55,14 +55,14 @@ android {
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
androidTestCompile "com.squareup.okhttp3:mockwebserver:$okhttp_version"
androidTestCompile 'com.android.support.test:runner:1.0.2'
androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"
androidTestImplementation 'com.android.support.test:runner:1.0.2'
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testCompile 'junit:junit:4.12'
testCompile "com.squareup.okhttp3:mockwebserver:$okhttp_version"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testImplementation 'junit:junit:4.12'
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"
}
......@@ -10,7 +10,6 @@ package at.bitfire.dav4android
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.property.GetETag
import at.bitfire.dav4android.property.SyncToken
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
......@@ -95,25 +94,25 @@ class DavCollectionTest {
" <D:sync-token>http://example.com/ns/sync/1234</D:sync-token>\n" +
" </D:multistatus>")
)
collection.reportChanges(null, false, null, GetETag.NAME)
val changes = collection.reportChanges(null, false, null, GetETag.NAME)
assertEquals(3, collection.members.size)
val members = collection.members.iterator()
assertEquals(3, changes.members.size)
val members = changes.members.iterator()
val member1 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("test.doc").build(), member1.location)
assertEquals("00001-abcd1", member1.properties[GetETag::class.java]!!.eTag)
assertEquals(sampleUrl().newBuilder().addPathSegment("test.doc").build(), member1.url)
assertEquals("00001-abcd1", member1[GetETag::class.java]!!.eTag)
val member2 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("vcard.vcf").build(), member2.location)
assertEquals("00002-abcd1", member2.properties[GetETag::class.java]!!.eTag)
assertEquals(sampleUrl().newBuilder().addPathSegment("vcard.vcf").build(), member2.url)
assertEquals("00002-abcd1", member2[GetETag::class.java]!!.eTag)
val member3 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("calendar.ics").build(), member3.location)
assertEquals("00003-abcd1", member3.properties[GetETag::class.java]!!.eTag)
assertEquals(sampleUrl().newBuilder().addPathSegment("calendar.ics").build(), member3.url)
assertEquals("00003-abcd1", member3[GetETag::class.java]!!.eTag)
assertEquals(0, collection.removedMembers.size)
assertFalse(collection.furtherResults)
assertEquals("http://example.com/ns/sync/1234", collection.properties[SyncToken::class.java]!!.token)
assertEquals(0, changes.removedMembers.size)
assertFalse(changes.furtherResults)
assertEquals("http://example.com/ns/sync/1234", changes.syncToken!!.token)
}
/**
......@@ -159,24 +158,24 @@ class DavCollectionTest {
" <D:sync-token>http://example.com/ns/sync/1233</D:sync-token>\n" +
" </D:multistatus>")
)
collection.reportChanges(null, false, null, GetETag.NAME)
val changes = collection.reportChanges(null, false, null, GetETag.NAME)
assertEquals(2, collection.members.size)
val members = collection.members.iterator()
assertEquals(2, changes.members.size)
val members = changes.members.iterator()
val member1 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("test.doc").build(), member1.location)
assertEquals("00001-abcd1", member1.properties[GetETag::class.java]!!.eTag)
assertEquals(sampleUrl().newBuilder().addPathSegment("test.doc").build(), member1.url)
assertEquals("00001-abcd1", member1[GetETag::class.java]!!.eTag)
val member2 = members.next()
assertEquals(sampleUrl().newBuilder().addPathSegment("vcard.vcf").build(), member2.location)
assertEquals("00002-abcd1", member2.properties[GetETag::class.java]!!.eTag)
assertEquals(sampleUrl().newBuilder().addPathSegment("vcard.vcf").build(), member2.url)
assertEquals("00002-abcd1", member2[GetETag::class.java]!!.eTag)
assertEquals(1, collection.removedMembers.size)
val removedMember = collection.removedMembers.first()
assertEquals(sampleUrl().newBuilder().addPathSegment("removed.txt").build(), removedMember.location)
assertEquals(1, changes.removedMembers.size)
val removedMember = changes.removedMembers.first()
assertEquals(sampleUrl().newBuilder().addPathSegment("removed.txt").build(), removedMember.url)
assertTrue(collection.furtherResults)
assertEquals("http://example.com/ns/sync/1233", collection.properties[SyncToken::class.java]!!.token)
assertTrue(changes.furtherResults)
assertEquals("http://example.com/ns/sync/1233", changes.syncToken!!.token)
}
/**
......
......@@ -10,6 +10,7 @@ package at.bitfire.dav4android
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.exception.InvalidDavResponseException
import okhttp3.*
import java.io.IOException
import java.io.StringWriter
......@@ -33,7 +34,7 @@ class DavAddressBook @JvmOverloads constructor(
* @throws HttpException on HTTP error
* @throws DavException on DAV error
*/
fun addressbookQuery() {
fun addressbookQuery(): DavResponse {
/* <!ELEMENT addressbook-query ((DAV:allprop |
DAV:propname |
DAV:prop)?, filter, limit?)>
......@@ -64,8 +65,11 @@ class DavAddressBook @JvmOverloads constructor(
checkStatus(response, false)
assertMultiStatus(response)
resetMembers()
response.body()?.charStream()?.use { processMultiStatus(it) }
response.body()?.charStream()?.use {
return processMultiStatus(it)
}
throw InvalidDavResponseException("Didn't receive 207 Multi-status response on REPORT addressbook-queryys")
}
/**
......@@ -74,7 +78,7 @@ class DavAddressBook @JvmOverloads constructor(
* @throws HttpException on HTTP error
* @throws DavException on DAV error
*/
fun multiget(urls: List<HttpUrl>, vCard4: Boolean) {
fun multiget(urls: List<HttpUrl>, vCard4: Boolean): DavResponse {
/* <!ELEMENT addressbook-multiget ((DAV:allprop |
DAV:propname |
DAV:prop)?,
......@@ -116,8 +120,11 @@ class DavAddressBook @JvmOverloads constructor(
checkStatus(response, false)
assertMultiStatus(response)
resetMembers()
response.body()?.charStream()?.use { processMultiStatus(it) }
response.body()?.charStream()?.use {
return processMultiStatus(it)
}
throw InvalidDavResponseException("Didn't receive 207 Multi-status response on REPORT addressbook-multiget")
}
}
......@@ -10,6 +10,7 @@ package at.bitfire.dav4android
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.exception.InvalidDavResponseException
import okhttp3.*
import java.io.IOException
import java.io.StringWriter
......@@ -40,7 +41,7 @@ class DavCalendar @JvmOverloads constructor(
* @throws HttpException on HTTP error
* @throws DavException on DAV error
*/
fun calendarQuery(component: String, start: Date?, end: Date?) {
fun calendarQuery(component: String, start: Date?, end: Date?): DavResponse {
/* <!ELEMENT calendar-query ((DAV:allprop |
DAV:propname |
DAV:prop)?, filter, timezone?)>
......@@ -91,8 +92,11 @@ class DavCalendar @JvmOverloads constructor(
checkStatus(response, false)
assertMultiStatus(response)
resetMembers()
response.body()?.charStream()?.use { processMultiStatus(it) }
response.body()?.charStream()?.use {
return processMultiStatus(it)
}
throw InvalidDavResponseException("Didn't receive 207 Multi-status response on REPORT calendar-query")
}
/**
......@@ -101,7 +105,7 @@ class DavCalendar @JvmOverloads constructor(
* @throws HttpException on HTTP error
* @throws DavException on DAV error
*/
fun multiget(urls: List<HttpUrl>) {
fun multiget(urls: List<HttpUrl>): DavResponse {
/* <!ELEMENT calendar-multiget ((DAV:allprop |
DAV:propname |
DAV:prop)?, DAV:href+)>
......@@ -137,8 +141,11 @@ class DavCalendar @JvmOverloads constructor(
checkStatus(response, false)
assertMultiStatus(response)
resetMembers()
response.body()?.charStream()?.use { processMultiStatus(it) }
response.body()?.charStream()?.use {
return processMultiStatus(it)
}
throw InvalidDavResponseException("Didn't receive 207 Multi-status response on REPORT calendar-multiget")
}
}
......@@ -10,6 +10,7 @@ package at.bitfire.dav4android
import at.bitfire.dav4android.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.exception.InvalidDavResponseException
import at.bitfire.dav4android.property.SyncToken
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
......@@ -37,7 +38,7 @@ open class DavCollection @JvmOverloads constructor(
* @throws DavException on DAV error
*/
fun reportChanges(syncToken: String?, infiniteDepth: Boolean, limit: Int?, vararg properties: Property.Name) {
fun reportChanges(syncToken: String?, infiniteDepth: Boolean, limit: Int?, vararg properties: Property.Name): DavResponse {
/* <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
<!ELEMENT sync-token CDATA> <!-- Text MUST be a URI -->
......@@ -85,8 +86,11 @@ open class DavCollection @JvmOverloads constructor(
checkStatus(response, false)
assertMultiStatus(response)
resetMembers()
response.body()?.charStream()?.use { processMultiStatus(it) }
response.body()?.charStream()?.use {
return processMultiStatus(it)
}
throw InvalidDavResponseException("Didn't receive 207 Multi-status response on REPORT sync-collection")
}
}
\ No newline at end of file
package at.bitfire.dav4android
import at.bitfire.dav4android.property.SyncToken
import okhttp3.HttpUrl
/**
* 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,
/** 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
) {
/**
* 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()
}
class Builder(
val url: HttpUrl
) {
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,
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 java.util.*
import java.util.Collections.unmodifiableMap
class PropertyCollection {
val properties = lazy { mutableMapOf<Property.Name, Property?>() }
/**
* Returns a WebDAV property, or null if this property is not known.
* In most cases, using the alternative [get] with [Class] parameter is better because it provides type safety.
*/
operator fun get(name: Property.Name): Property? =
if (!properties.isInitialized())
null
else
properties.value[name]
/**
* Returns a WebDAV property, or null if this property is not known.
*/
operator fun<T: Property> get(clazz: Class<T>): T? {
if (!properties.isInitialized())
return null
return try {
val name = clazz.getDeclaredField("NAME").get(null) as Property.Name
properties.value[name] as? T
} catch (e: NoSuchFieldException) {
Constants.log.severe("$clazz does not have a static NAME field")
null
}
}
private fun getMap(): Map<Property.Name, Property?> =
if (!properties.isInitialized())
mapOf()
else
unmodifiableMap(properties.value)
operator fun set(name: Property.Name, property: Property?) {
properties.value[name] = property
}
operator fun minusAssign(name: Property.Name) {
if (!properties.isInitialized())
return
properties.value.remove(name)
}
fun size() =
if (!properties.isInitialized())
0
else
properties.value.size
/**
* Merges another [PropertyCollection] into [properties].
* Existing properties will be overwritten.
*
* @param another property collection to take the properties from
* @param removeNullValues Indicates how "another" properties with null values should be treated.
* - true: If the "another" property value is null, the property will be removed in [properties].
* - false: If the "another" property value is null, the property in [properties] will be set to null, too,
* but only if it doesn't exist yet. This means values in [properties] will never be overwritten by null.
*/
fun merge(another: PropertyCollection, removeNullValues: Boolean) {
val properties = another.getMap()
for ((name, prop) in properties) {
if (prop != null)
set(name, prop)
else {
// prop == null
if (removeNullValues)
this -= name
else if (get(name) == null) // never overwrite non-null values
set(name, null)
}
}
}
fun nullAllValues() {
if (!properties.isInitialized())
return
val props = properties.value
for (name in props.keys)
props[name] = null
}
override fun toString(): String {
val s = LinkedList<String>()
for ((name, value) in getMap())
s.add("$name = $value")
return "[${s.joinToString(", ")}]"
}
}
/*
* 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.exception
class UnsupportedDavException(message: String): DavException(message)
\ 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