Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 23285c75 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

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
parent 0db22c7d
Loading
Loading
Loading
Loading
+1 −42
Original line number Diff line number Diff line
@@ -11,7 +11,6 @@
package at.bitfire.dav4jvm.ktor

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

        return processMultiStatus(bodyChannel, callback)
    }

    /**
     * Processes a Multi-Status response.
     *
     * @param bodyChannel   the response body channel to read the Multi-Status response from
     * @param callback      called for every XML response element in the Multi-Status response
     *
     * @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 IOException on I/O error
     * @throws HttpException on HTTP error
     * @throws DavException on WebDAV error (like an invalid XML response)
     */
    protected suspend fun processMultiStatus(bodyChannel: ByteReadChannel, callback: MultiResponseCallback): List<Property> {
        val parser = XmlUtils.newPullParser()

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

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

    private suspend fun parseMultiStatus(parser: XmlPullParser, callback: MultiResponseCallback): List<Property> {
        val responseProperties = mutableListOf<Property>()

        // <!ELEMENT multistatus (response*, responsedescription?,
        //                        sync-token?) >
        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)
                when (parser.propertyName()) {
                    WebDAV.Response ->
                        Response.parse(parser, location, callback)
                    WebDAV.SyncToken ->
                        XmlReader(parser).readText()?.let {
                            responseProperties += SyncToken(it)
                        }
                }
            eventType = parser.next()
        }

        return responseProperties
    }

}
 No newline at end of file
+55 −0
Original line number Diff line number Diff line
/*
 * 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.Property
import at.bitfire.dav4jvm.XmlReader
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.SyncToken
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.Url
import org.xmlpull.v1.XmlPullParser

/**
 * Parses a WebDAV `<multistatus>` XML response.
 *
 * @param location  location of the request (used to resolve possible relative `<href>` in responses)
 */
class MultiStatusParser(
    private val location: Url,
    private val callback: MultiResponseCallback
) {

    suspend fun parseResponse(parser: XmlPullParser): List<Property> {
        val responseProperties = mutableListOf<Property>()
        val responseParser = ResponseParser(location, callback)

        // <!ELEMENT multistatus (response*, responsedescription?,
        //                        sync-token?) >
        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)
                when (parser.propertyName()) {
                    WebDAV.Response ->
                        responseParser.parseResponse(parser)
                    WebDAV.SyncToken ->
                        XmlReader(parser).readText()?.let {
                            responseProperties += SyncToken(it)
                        }
                }
            eventType = parser.next()
        }

        return responseProperties
    }

}
 No newline at end of file
+1 −33
Original line number Diff line number Diff line
@@ -12,11 +12,7 @@ package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Error
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.HttpStatusCode
import org.xmlpull.v1.XmlPullParser
import java.util.LinkedList

/**
 * Represents a WebDAV propstat XML element.
@@ -27,32 +23,4 @@ data class PropStat(
    val properties: List<Property>,
    val status: HttpStatusCode,
    val error: List<Error>? = null
) {

    companion object {

        private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK")

        fun parse(parser: XmlPullParser): PropStat {
            val depth = parser.depth

            var status: HttpStatusCode? = 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)
                    when (parser.propertyName()) {
                        WebDAV.Prop ->
                            prop.addAll(Property.parse(parser))
                        WebDAV.Status ->
                            status = KtorHttpUtils.parseStatusLine(parser.nextText())
                    }
                eventType = parser.next()
            }

            return PropStat(prop, status ?: ASSUMING_OK)
        }

    }
}
 No newline at end of file
)
 No newline at end of file
+45 −0
Original line number Diff line number Diff line
/*
 * 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.Property
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.HttpStatusCode
import org.xmlpull.v1.XmlPullParser
import java.util.LinkedList

object PropStatParser {

    private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK")

    fun parse(parser: XmlPullParser): PropStat {
        val depth = parser.depth

        var status: HttpStatusCode? = 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)
                when (parser.propertyName()) {
                    WebDAV.Prop ->
                        prop.addAll(Property.parse(parser))
                    WebDAV.Status ->
                        status = KtorHttpUtils.parseStatusLine(parser.nextText())
                }
            eventType = parser.next()
        }

        return PropStat(prop, status ?: ASSUMING_OK)
    }

}
 No newline at end of file
+0 −147
Original line number Diff line number Diff line
@@ -12,16 +12,9 @@ package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Error
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.isSuccess
import io.ktor.http.takeFrom
import org.xmlpull.v1.XmlPullParser
import java.util.logging.Logger

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


    companion object {

        /**
         * Parses an XML response element and calls the [callback] for it (when it has a `<href>`).
         * The arguments of the [MultiResponseCallback.onResponse] are set accordingly.
         *
         * If the [at.bitfire.dav4jvm.property.webdav.ResourceType] of the queried resource is known (= was queried and returned by the server)
         * and it contains [at.bitfire.dav4jvm.property.webdav.ResourceType.Companion.COLLECTION], the `href` property of the callback will automatically
         * have a trailing slash.
         *
         * So if you want PROPFIND results to have a trailing slash when they are collections, make sure
         * that you query [at.bitfire.dav4jvm.property.webdav.ResourceType].
         */
        suspend fun parse(parser: XmlPullParser, location: Url, callback: MultiResponseCallback) {
            val logger = Logger.getLogger(Response::javaClass.name)

            val depth = parser.depth

            var hrefOrNull: Url? = null
            var status: HttpStatusCode? = null
            val propStat = mutableListOf<PropStat>()
            var error: List<Error>? = null
            var newLocation: Url? = null

            var eventType = parser.eventType
            while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
                if (eventType == XmlPullParser.START_TAG && parser.depth == depth+1)
                    when (parser.propertyName()) {
                        WebDAV.Href -> {
                            var sHref = parser.nextText()
                            var hierarchical = false
                            if (!sHref.startsWith("/")) {
                                /* According to RFC 4918 8.3 URL Handling, only absolute paths are allowed as relative
                                   URLs. However, some servers reply with relative paths. */
                                val firstColon = sHref.indexOf(':')
                                if (firstColon != -1) {
                                    /* There are some servers which return not only relative paths, but relative paths like "a:b.vcf",
                                       which would be interpreted as scheme: "a", scheme-specific part: "b.vcf" normally.
                                       For maximum compatibility, we prefix all relative paths which contain ":" (but not "://"),
                                       with "./" to allow resolving by HttpUrl. */
                                    try {
                                        if (sHref.substring(firstColon, firstColon + 3) == "://")
                                            hierarchical = true
                                    } catch (e: IndexOutOfBoundsException) {
                                        // no "://"
                                    }
                                    if (!hierarchical)
                                        sHref = "./$sHref"

                                }
                            }


                            if(!hierarchical) {
                                val urlBuilder = URLBuilder(location).takeFrom(sHref)
                                urlBuilder.pathSegments = urlBuilder.pathSegments.filterNot { it == "." } // Drop segments that are "./"
                                hrefOrNull = urlBuilder.build()
                            } else {
                                hrefOrNull = URLBuilder(location).takeFrom(sHref).build()
                            }
                        }
                        WebDAV.Status ->
                            status = KtorHttpUtils.parseStatusLine(parser.nextText())
                        WebDAV.PropStat ->
                            PropStat.parse(parser).let { propStat += it }
                        WebDAV.Error ->
                            error = Error.parseError(parser)
                        WebDAV.Location ->
                            newLocation = Url(parser.nextText())    // TODO: Need to catch exception here?
                        }
                eventType = parser.next()
            }

            if (hrefOrNull == null) {
                logger.warning("Ignoring XML response element without valid href")
                return
            }
            var href: Url = hrefOrNull      // guaranteed to be not null

            // if we know this resource is a collection, make sure href has a trailing slash
            // (for clarity and resolving relative paths)
            propStat.filter { it.status.isSuccess() }
                .flatMap { it.properties }
                .filterIsInstance<ResourceType>()
                .firstOrNull()
                ?.let { type ->
                    if (type.types.contains(WebDAV.Collection))
                        href = UrlUtils.withTrailingSlash(href)
                }

            //log.log(Level.FINE, "Received properties for $href", if (status != null) status else propStat)

            // Which resource does this <response> represent?
            val relation = when {
                UrlUtils.omitTrailingSlash(href).equalsForWebDAV(UrlUtils.omitTrailingSlash(location)) ->
                    HrefRelation.SELF

                else -> {
                    if (location.protocol.name == href.protocol.name && location.host == href.host && location.port == href.port) {
                        val locationSegments = location.rawSegments
                        val hrefSegments = href.rawSegments

                        // don't compare trailing slash segment ("")
                        var nBasePathSegments = locationSegments.size
                        if (locationSegments.lastOrNull() == "")
                            nBasePathSegments--

                        /* example:   locationSegments  = [ "davCollection", "" ]
                                      nBasePathSegments = 1
                                      hrefSegments      = [ "davCollection", "aMember" ]
                        */
                        var relation = HrefRelation.OTHER
                        if (hrefSegments.size > nBasePathSegments) {
                            val sameBasePath = (0 until nBasePathSegments).none { locationSegments[it] != hrefSegments[it] }
                            if (sameBasePath)
                                relation = HrefRelation.MEMBER
                        }

                        relation
                    } else
                        HrefRelation.OTHER
                }
            }

            callback.onResponse(
                Response(
                    requestedUrl = location,
                    href = href,
                    status = status,
                    propstat = propStat,
                    error = error,
                    newLocation = newLocation
                ),
                relation
            )
        }

    }

}
 No newline at end of file
Loading