Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0942875
Replacing okhttp with ktor [wip]
patrickunterwegs Jul 31, 2025
135768b
Merge remote-tracking branch 'upstream/main'
patrickunterwegs Jul 31, 2025
be2bf2a
Fixed test and handling of serializable Status Codes
patrickunterwegs Aug 1, 2025
96e0e7e
Incorporated feedback from Ricki
patrickunterwegs Aug 1, 2025
3ccdaaf
Updated DavException.kt
patrickunterwegs Aug 1, 2025
29c1677
Removed obsolete TODO
patrickunterwegs Aug 1, 2025
d03539f
Removed obsolete comments
patrickunterwegs Aug 1, 2025
049a3f6
Removed more okhttp references
patrickunterwegs Aug 1, 2025
fc8b155
Split DavResourceTest.kt in smaller tests
patrickunterwegs Aug 7, 2025
46fc476
Merge remote-tracking branch 'upstream/main'
patrickunterwegs Aug 7, 2025
c98b450
Removed BasicDigestAuthHandler as this should now be handled by ktor
patrickunterwegs Aug 7, 2025
3b4a379
Completely removed OKHttp
patrickunterwegs Aug 7, 2025
306a0b6
Moved all files to dedicated ktor package
patrickunterwegs Aug 8, 2025
41b4d8e
Took care of some todos
patrickunterwegs Aug 8, 2025
2da8b5b
Merge remote-tracking branch 'upstream/main'
patrickunterwegs Aug 18, 2025
05992c2
restored okhttp files in dedicated package
patrickunterwegs Aug 18, 2025
104790f
Merge remote-tracking branch 'upstream/main'
patrickunterwegs Aug 26, 2025
2f39e11
Merged latest changes
patrickunterwegs Aug 26, 2025
e76dfd5
Merge remote-tracking branch 'upstream/main'
patrickunterwegs Aug 30, 2025
c59615c
Merge remote-tracking branch 'upstream/main'
patrickunterwegs Sep 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ tasks.withType<DokkaTask>().configureEach {
}

dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.server.http.redirect)
implementation(libs.slf4j)
api(libs.okhttp)
api(libs.spotbugs.annotations)
api(libs.xpp3)

testImplementation(libs.junit4)
testImplementation(libs.ktor.client.mock)
testImplementation(libs.okhttp.mockwebserver)
}
9 changes: 8 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ dokka = "2.0.0"
junit4 = "4.13.2"
kotlin = "2.2.10"
okhttpVersion = "5.1.0"
ktor = "3.2.1"
slf4j = "1.7.36"
spotbugs = "4.9.4"
xpp3Version = "1.1.6"

[libraries]
junit4 = { module = "junit:junit", version.ref = "junit4" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-server-http-redirect = { module = "io.ktor:ktor-server-http-redirect", version.ref = "ktor" }
ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttpVersion" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver3", version.ref = "okhttpVersion" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttpVersion" }
slf4j = { module = "org.slf4j:slf4j-android", version.ref = "slf4j" }
spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version.ref = "spotbugs" }
xpp3 = { module = "org.ogce:xpp3", version.ref = "xpp3Version" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
* SPDX-License-Identifier: MPL-2.0
*/

package at.bitfire.dav4jvm
package at.bitfire.dav4jvm.ktor

import io.ktor.client.statement.HttpResponse

/**
* Callback for the OPTIONS request.
*/
fun interface CapabilitiesCallback {
fun onCapabilities(davCapabilities: Set<String>, response: okhttp3.Response)
fun onCapabilities(davCapabilities: Set<String>, response: HttpResponse)
}

/**
Expand All @@ -26,7 +28,7 @@ fun interface MultiResponseCallback {
* in response to a `PROPFIND` request, this callback will be called once for every found
* member resource.
*
* Known collections have [response] `href` with trailing slash, see [at.bitfire.dav4jvm.Response.parse] for details.
* Known collections have [response] `href` with trailing slash, see [Response.parse] for details.
*
* @param response the parsed response (including URL)
* @param relation relation of the response to the called resource
Expand All @@ -42,5 +44,5 @@ fun interface ResponseCallback {
* Called for a HTTP response. Typically this is only called for successful/redirect
* responses because HTTP errors throw an exception before this callback is called.
*/
fun onResponse(response: okhttp3.Response)
fun onResponse(response: HttpResponse)
}
153 changes: 153 additions & 0 deletions src/main/kotlin/at/bitfire/dav4jvm/ktor/DavAddressBook.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*
* 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 https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.ktor.XmlUtils.insertTag
import at.bitfire.dav4jvm.ktor.property.carddav.AddressData
import at.bitfire.dav4jvm.ktor.property.carddav.NS_CARDDAV
import at.bitfire.dav4jvm.ktor.property.webdav.GetContentType
import at.bitfire.dav4jvm.ktor.property.webdav.GetETag
import at.bitfire.dav4jvm.ktor.property.webdav.NS_WEBDAV
import io.ktor.client.HttpClient
import io.ktor.client.request.prepareRequest
import io.ktor.client.request.setBody
import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.Url
import io.ktor.util.logging.Logger
import org.slf4j.LoggerFactory
import java.io.StringWriter

@Suppress("unused")
class DavAddressBook @JvmOverloads constructor(
httpClient: HttpClient,
location: Url,
logger: Logger = LoggerFactory.getLogger(DavAddressBook::javaClass.name)
): DavCollection(httpClient, location, logger) {

companion object {
val MIME_JCARD = ContentType.Companion.parse("application/vcard+json")
val MIME_VCARD3_UTF8 = ContentType.Companion.parse("text/vcard;charset=utf-8")
val MIME_VCARD4 = ContentType.Companion.parse("text/vcard;version=4.0")

val ADDRESSBOOK_QUERY = Property.Name(NS_CARDDAV, "addressbook-query")
val ADDRESSBOOK_MULTIGET = Property.Name(NS_CARDDAV, "addressbook-multiget")
val FILTER = Property.Name(NS_CARDDAV, "filter")
}

/**
* 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 java.io.IOException on I/O error
* @throws at.bitfire.dav4jvm.ktor.exception.HttpException on HTTP error
* @throws at.bitfire.dav4jvm.ktor.exception.DavException on WebDAV error
*/
suspend fun addressbookQuery(callback: MultiResponseCallback): List<Property> {
/* <!ELEMENT addressbook-query ((DAV:allprop |
DAV:propname |
DAV:prop)?, filter, limit?)>
<!ELEMENT filter (prop-filter*)>
*/
val serializer = XmlUtils.newSerializer()
val writer = StringWriter()
serializer.setOutput(writer)
serializer.startDocument("UTF-8", null)
serializer.setPrefix("", NS_WEBDAV)
serializer.setPrefix("CARD", NS_CARDDAV)
serializer.insertTag(ADDRESSBOOK_QUERY) {
insertTag(PROP) {
insertTag(GetETag.Companion.NAME)
}
insertTag(FILTER)
}
serializer.endDocument()

followRedirects {
httpClient.prepareRequest {
url(location)
method = HttpMethod.Companion.parse("REPORT")
headers.append(HttpHeaders.ContentType, MIME_XML.toString())
setBody(writer.toString())
headers.append(HttpHeaders.Depth, "1")
}.execute()
}.let { response ->
return processMultiStatus(response, callback)
}
}

/**
* Sends an addressbook-multiget REPORT request to the resource.
*
* @param urls list of vCard URLs to be requested
* @param contentType MIME type of requested format; may be "text/vcard" for vCard or
* "application/vcard+json" for jCard. *null*: don't request specific representation type
* @param version vCard version subtype of the requested format. Should only be specified together with a [contentType] of "text/vcard".
* Currently only useful value: "4.0" for vCard 4. *null*: don't request specific version
* @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 java.io.IOException on I/O error
* @throws at.bitfire.dav4jvm.ktor.exception.HttpException on HTTP error
* @throws at.bitfire.dav4jvm.ktor.exception.DavException on WebDAV error
*/
suspend fun multiget(urls: List<Url>, contentType: String? = null, version: String? = null, callback: MultiResponseCallback): List<Property> {
/* <!ELEMENT addressbook-multiget ((DAV:allprop |
DAV:propname |
DAV:prop)?,
DAV:href+)>
*/
val serializer = XmlUtils.newSerializer()
val writer = StringWriter()
serializer.setOutput(writer)
serializer.startDocument("UTF-8", null)
serializer.setPrefix("", NS_WEBDAV)
serializer.setPrefix("CARD", NS_CARDDAV)
serializer.insertTag(ADDRESSBOOK_MULTIGET) {
insertTag(PROP) {
insertTag(GetContentType.Companion.NAME)
insertTag(GetETag.Companion.NAME)
insertTag(AddressData.Companion.NAME) {
if (contentType != null)
attribute(null, AddressData.Companion.CONTENT_TYPE, contentType)
if (version != null)
attribute(null, AddressData.Companion.VERSION, version)
}
}
for (url in urls)
insertTag(HREF) {
text(url.encodedPath)
}
}
serializer.endDocument()

followRedirects {
httpClient.prepareRequest {
url(location)
method = HttpMethod.Companion.parse("REPORT")
setBody(writer.toString())
headers.append(HttpHeaders.ContentType, MIME_XML.toString())
headers.append(HttpHeaders.Depth, "0")
}.execute()
}.let { response ->
return processMultiStatus(response, callback)
}
}

}
Loading