Skip to content

Commit 23285c7

Browse files
authored
Separate data classes and parser (#133)
* Extract MultiStatusParser * Extract ResponseParser and PropStatParser * Refactor parsers and data classes - Move `PropStatParser` to object-based implementation - Update `MultiStatusParser` and `ResponseParser` to use callback directly - Remove redundant parameters from parsing functions
1 parent 0db22c7 commit 23285c7

File tree

6 files changed

+273
-222
lines changed

6 files changed

+273
-222
lines changed

src/main/kotlin/at/bitfire/dav4jvm/ktor/DavResource.kt

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
package at.bitfire.dav4jvm.ktor
1212

1313
import at.bitfire.dav4jvm.Property
14-
import at.bitfire.dav4jvm.XmlReader
1514
import at.bitfire.dav4jvm.XmlUtils
1615
import at.bitfire.dav4jvm.XmlUtils.insertTag
1716
import at.bitfire.dav4jvm.XmlUtils.propertyName
@@ -735,23 +734,6 @@ open class DavResource(
735734
// verify that the response is 207 Multi-Status
736735
assertMultiStatus(response, bodyChannel)
737736

738-
return processMultiStatus(bodyChannel, callback)
739-
}
740-
741-
/**
742-
* Processes a Multi-Status response.
743-
*
744-
* @param bodyChannel the response body channel to read the Multi-Status response from
745-
* @param callback called for every XML response element in the Multi-Status response
746-
*
747-
* @return list of properties which have been received in the Multi-Status response, but
748-
* are not part of response XML elements (like `sync-token` which is returned as [SyncToken])
749-
*
750-
* @throws IOException on I/O error
751-
* @throws HttpException on HTTP error
752-
* @throws DavException on WebDAV error (like an invalid XML response)
753-
*/
754-
protected suspend fun processMultiStatus(bodyChannel: ByteReadChannel, callback: MultiResponseCallback): List<Property> {
755737
val parser = XmlUtils.newPullParser()
756738

757739
try {
@@ -762,7 +744,7 @@ open class DavResource(
762744
while (eventType != XmlPullParser.END_DOCUMENT) {
763745
if (eventType == XmlPullParser.START_TAG && parser.depth == 1)
764746
if (parser.propertyName() == WebDAV.MultiStatus) {
765-
return parseMultiStatus(parser, callback)
747+
return MultiStatusParser(location, callback).parseResponse(parser)
766748
// further <multistatus> elements are ignored
767749
}
768750

@@ -779,27 +761,4 @@ open class DavResource(
779761
}
780762
}
781763

782-
private suspend fun parseMultiStatus(parser: XmlPullParser, callback: MultiResponseCallback): List<Property> {
783-
val responseProperties = mutableListOf<Property>()
784-
785-
// <!ELEMENT multistatus (response*, responsedescription?,
786-
// sync-token?) >
787-
val depth = parser.depth
788-
var eventType = parser.eventType
789-
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
790-
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
791-
when (parser.propertyName()) {
792-
WebDAV.Response ->
793-
Response.parse(parser, location, callback)
794-
WebDAV.SyncToken ->
795-
XmlReader(parser).readText()?.let {
796-
responseProperties += SyncToken(it)
797-
}
798-
}
799-
eventType = parser.next()
800-
}
801-
802-
return responseProperties
803-
}
804-
805764
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* SPDX-License-Identifier: MPL-2.0
9+
*/
10+
11+
package at.bitfire.dav4jvm.ktor
12+
13+
import at.bitfire.dav4jvm.Property
14+
import at.bitfire.dav4jvm.XmlReader
15+
import at.bitfire.dav4jvm.XmlUtils.propertyName
16+
import at.bitfire.dav4jvm.property.webdav.SyncToken
17+
import at.bitfire.dav4jvm.property.webdav.WebDAV
18+
import io.ktor.http.Url
19+
import org.xmlpull.v1.XmlPullParser
20+
21+
/**
22+
* Parses a WebDAV `<multistatus>` XML response.
23+
*
24+
* @param location location of the request (used to resolve possible relative `<href>` in responses)
25+
*/
26+
class MultiStatusParser(
27+
private val location: Url,
28+
private val callback: MultiResponseCallback
29+
) {
30+
31+
suspend fun parseResponse(parser: XmlPullParser): List<Property> {
32+
val responseProperties = mutableListOf<Property>()
33+
val responseParser = ResponseParser(location, callback)
34+
35+
// <!ELEMENT multistatus (response*, responsedescription?,
36+
// sync-token?) >
37+
val depth = parser.depth
38+
var eventType = parser.eventType
39+
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
40+
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
41+
when (parser.propertyName()) {
42+
WebDAV.Response ->
43+
responseParser.parseResponse(parser)
44+
WebDAV.SyncToken ->
45+
XmlReader(parser).readText()?.let {
46+
responseProperties += SyncToken(it)
47+
}
48+
}
49+
eventType = parser.next()
50+
}
51+
52+
return responseProperties
53+
}
54+
55+
}

src/main/kotlin/at/bitfire/dav4jvm/ktor/PropStat.kt

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ package at.bitfire.dav4jvm.ktor
1212

1313
import at.bitfire.dav4jvm.Error
1414
import at.bitfire.dav4jvm.Property
15-
import at.bitfire.dav4jvm.XmlUtils.propertyName
16-
import at.bitfire.dav4jvm.property.webdav.WebDAV
1715
import io.ktor.http.HttpStatusCode
18-
import org.xmlpull.v1.XmlPullParser
19-
import java.util.LinkedList
2016

2117
/**
2218
* Represents a WebDAV propstat XML element.
@@ -27,32 +23,4 @@ data class PropStat(
2723
val properties: List<Property>,
2824
val status: HttpStatusCode,
2925
val error: List<Error>? = null
30-
) {
31-
32-
companion object {
33-
34-
private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK")
35-
36-
fun parse(parser: XmlPullParser): PropStat {
37-
val depth = parser.depth
38-
39-
var status: HttpStatusCode? = null
40-
val prop = LinkedList<Property>()
41-
42-
var eventType = parser.eventType
43-
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
44-
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
45-
when (parser.propertyName()) {
46-
WebDAV.Prop ->
47-
prop.addAll(Property.parse(parser))
48-
WebDAV.Status ->
49-
status = KtorHttpUtils.parseStatusLine(parser.nextText())
50-
}
51-
eventType = parser.next()
52-
}
53-
54-
return PropStat(prop, status ?: ASSUMING_OK)
55-
}
56-
57-
}
58-
}
26+
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* SPDX-License-Identifier: MPL-2.0
9+
*/
10+
11+
package at.bitfire.dav4jvm.ktor
12+
13+
import at.bitfire.dav4jvm.Property
14+
import at.bitfire.dav4jvm.XmlUtils.propertyName
15+
import at.bitfire.dav4jvm.property.webdav.WebDAV
16+
import io.ktor.http.HttpStatusCode
17+
import org.xmlpull.v1.XmlPullParser
18+
import java.util.LinkedList
19+
20+
object PropStatParser {
21+
22+
private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK")
23+
24+
fun parse(parser: XmlPullParser): PropStat {
25+
val depth = parser.depth
26+
27+
var status: HttpStatusCode? = null
28+
val prop = LinkedList<Property>()
29+
30+
var eventType = parser.eventType
31+
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
32+
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
33+
when (parser.propertyName()) {
34+
WebDAV.Prop ->
35+
prop.addAll(Property.parse(parser))
36+
WebDAV.Status ->
37+
status = KtorHttpUtils.parseStatusLine(parser.nextText())
38+
}
39+
eventType = parser.next()
40+
}
41+
42+
return PropStat(prop, status ?: ASSUMING_OK)
43+
}
44+
45+
}

src/main/kotlin/at/bitfire/dav4jvm/ktor/Response.kt

Lines changed: 0 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,9 @@ package at.bitfire.dav4jvm.ktor
1212

1313
import at.bitfire.dav4jvm.Error
1414
import at.bitfire.dav4jvm.Property
15-
import at.bitfire.dav4jvm.XmlUtils.propertyName
16-
import at.bitfire.dav4jvm.property.webdav.ResourceType
17-
import at.bitfire.dav4jvm.property.webdav.WebDAV
1815
import io.ktor.http.HttpStatusCode
19-
import io.ktor.http.URLBuilder
2016
import io.ktor.http.Url
2117
import io.ktor.http.isSuccess
22-
import io.ktor.http.takeFrom
23-
import org.xmlpull.v1.XmlPullParser
24-
import java.util.logging.Logger
2518

2619
/**
2720
* Represents a WebDAV response XML Element.
@@ -97,144 +90,4 @@ data class Response(
9790
*/
9891
fun hrefName() = KtorHttpUtils.fileName(href)
9992

100-
101-
companion object {
102-
103-
/**
104-
* Parses an XML response element and calls the [callback] for it (when it has a `<href>`).
105-
* The arguments of the [MultiResponseCallback.onResponse] are set accordingly.
106-
*
107-
* If the [at.bitfire.dav4jvm.property.webdav.ResourceType] of the queried resource is known (= was queried and returned by the server)
108-
* and it contains [at.bitfire.dav4jvm.property.webdav.ResourceType.Companion.COLLECTION], the `href` property of the callback will automatically
109-
* have a trailing slash.
110-
*
111-
* So if you want PROPFIND results to have a trailing slash when they are collections, make sure
112-
* that you query [at.bitfire.dav4jvm.property.webdav.ResourceType].
113-
*/
114-
suspend fun parse(parser: XmlPullParser, location: Url, callback: MultiResponseCallback) {
115-
val logger = Logger.getLogger(Response::javaClass.name)
116-
117-
val depth = parser.depth
118-
119-
var hrefOrNull: Url? = null
120-
var status: HttpStatusCode? = null
121-
val propStat = mutableListOf<PropStat>()
122-
var error: List<Error>? = null
123-
var newLocation: Url? = null
124-
125-
var eventType = parser.eventType
126-
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
127-
if (eventType == XmlPullParser.START_TAG && parser.depth == depth+1)
128-
when (parser.propertyName()) {
129-
WebDAV.Href -> {
130-
var sHref = parser.nextText()
131-
var hierarchical = false
132-
if (!sHref.startsWith("/")) {
133-
/* According to RFC 4918 8.3 URL Handling, only absolute paths are allowed as relative
134-
URLs. However, some servers reply with relative paths. */
135-
val firstColon = sHref.indexOf(':')
136-
if (firstColon != -1) {
137-
/* There are some servers which return not only relative paths, but relative paths like "a:b.vcf",
138-
which would be interpreted as scheme: "a", scheme-specific part: "b.vcf" normally.
139-
For maximum compatibility, we prefix all relative paths which contain ":" (but not "://"),
140-
with "./" to allow resolving by HttpUrl. */
141-
try {
142-
if (sHref.substring(firstColon, firstColon + 3) == "://")
143-
hierarchical = true
144-
} catch (e: IndexOutOfBoundsException) {
145-
// no "://"
146-
}
147-
if (!hierarchical)
148-
sHref = "./$sHref"
149-
150-
}
151-
}
152-
153-
154-
if(!hierarchical) {
155-
val urlBuilder = URLBuilder(location).takeFrom(sHref)
156-
urlBuilder.pathSegments = urlBuilder.pathSegments.filterNot { it == "." } // Drop segments that are "./"
157-
hrefOrNull = urlBuilder.build()
158-
} else {
159-
hrefOrNull = URLBuilder(location).takeFrom(sHref).build()
160-
}
161-
}
162-
WebDAV.Status ->
163-
status = KtorHttpUtils.parseStatusLine(parser.nextText())
164-
WebDAV.PropStat ->
165-
PropStat.parse(parser).let { propStat += it }
166-
WebDAV.Error ->
167-
error = Error.parseError(parser)
168-
WebDAV.Location ->
169-
newLocation = Url(parser.nextText()) // TODO: Need to catch exception here?
170-
}
171-
eventType = parser.next()
172-
}
173-
174-
if (hrefOrNull == null) {
175-
logger.warning("Ignoring XML response element without valid href")
176-
return
177-
}
178-
var href: Url = hrefOrNull // guaranteed to be not null
179-
180-
// if we know this resource is a collection, make sure href has a trailing slash
181-
// (for clarity and resolving relative paths)
182-
propStat.filter { it.status.isSuccess() }
183-
.flatMap { it.properties }
184-
.filterIsInstance<ResourceType>()
185-
.firstOrNull()
186-
?.let { type ->
187-
if (type.types.contains(WebDAV.Collection))
188-
href = UrlUtils.withTrailingSlash(href)
189-
}
190-
191-
//log.log(Level.FINE, "Received properties for $href", if (status != null) status else propStat)
192-
193-
// Which resource does this <response> represent?
194-
val relation = when {
195-
UrlUtils.omitTrailingSlash(href).equalsForWebDAV(UrlUtils.omitTrailingSlash(location)) ->
196-
HrefRelation.SELF
197-
198-
else -> {
199-
if (location.protocol.name == href.protocol.name && location.host == href.host && location.port == href.port) {
200-
val locationSegments = location.rawSegments
201-
val hrefSegments = href.rawSegments
202-
203-
// don't compare trailing slash segment ("")
204-
var nBasePathSegments = locationSegments.size
205-
if (locationSegments.lastOrNull() == "")
206-
nBasePathSegments--
207-
208-
/* example: locationSegments = [ "davCollection", "" ]
209-
nBasePathSegments = 1
210-
hrefSegments = [ "davCollection", "aMember" ]
211-
*/
212-
var relation = HrefRelation.OTHER
213-
if (hrefSegments.size > nBasePathSegments) {
214-
val sameBasePath = (0 until nBasePathSegments).none { locationSegments[it] != hrefSegments[it] }
215-
if (sameBasePath)
216-
relation = HrefRelation.MEMBER
217-
}
218-
219-
relation
220-
} else
221-
HrefRelation.OTHER
222-
}
223-
}
224-
225-
callback.onResponse(
226-
Response(
227-
requestedUrl = location,
228-
href = href,
229-
status = status,
230-
propstat = propStat,
231-
error = error,
232-
newLocation = newLocation
233-
),
234-
relation
235-
)
236-
}
237-
238-
}
239-
24093
}

0 commit comments

Comments
 (0)