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

Commit 04250fef authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Refactor exceptions

parent ac0cdc7b
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -59,7 +59,6 @@ dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
    implementation "commons-io:commons-io:2.6"

    androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+2 −2
Original line number Diff line number Diff line
@@ -24,13 +24,13 @@ class DavCollectionTest {
    private val mockServer = MockWebServer()
    private fun sampleUrl() = mockServer.url("/dav/")


    @Before
    fun startServer() = mockServer.start()

    @After
    fun stopServer() = mockServer.shutdown()


    /**
     * Test sample response for an initial sync-collection report from RFC 6578 3.8.
     */
@@ -197,7 +197,7 @@ class DavCollectionTest {
            collection.reportChanges("http://example.com/ns/sync/1232", false, 100, GetETag.NAME).close()
            fail("Expected HttpException")
        } catch (e: HttpException) {
            assertEquals(507, e.status)
            assertEquals(507, e.code)
        }
    }

+113 −0
Original line number Diff line number Diff line
/*
 * 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.exception.DavException
import at.bitfire.dav4android.exception.HttpException
import at.bitfire.dav4android.property.ResourceType
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test

class DavExceptionTest {

    private val httpClient = OkHttpClient.Builder()
            .followRedirects(false)
            .build()
    private val mockServer = MockWebServer()
    private fun sampleUrl() = mockServer.url("/dav/")

    @Before
    fun startServer() = mockServer.start()

    @After
    fun stopServer() = mockServer.shutdown()


    /**
     * Test a large HTML response which has a multi-octet UTF-8 character
     * exactly at the cut-off position.
     */
    @Test
    fun testLargeTextError() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)

        val builder = StringBuilder()
        builder.append(CharArray(DavException.MAX_EXCERPT_SIZE-1, { '*' }))
        builder.append("\u03C0")    // Pi
        val body = builder.toString()

        mockServer.enqueue(MockResponse()
                .setResponseCode(404)
                .setHeader("Content-Type", "text/html")
                .setBody(body))
        try {
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected HttpException")
        } catch (e: HttpException) {
            assertEquals(e.code, 404)
            assertTrue(e.errors.isEmpty())
            assertEquals(
                    body.substring(0, DavException.MAX_EXCERPT_SIZE-1),
                    e.responseBody!!.substring(0, DavException.MAX_EXCERPT_SIZE-1)
            )
        }
    }

    @Test
    fun testNonTextError() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)

        mockServer.enqueue(MockResponse()
                .setResponseCode(403)
                .setHeader("Content-Type", "application/octet-stream")
                .setBody("12345"))
        try {
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected HttpException")
        } catch (e: HttpException) {
            assertEquals(e.code, 403)
            assertTrue(e.errors.isEmpty())
            assertNull(e.responseBody)
        }
    }

    /**
     * Test precondition XML element (sample from RFC 4918 16)
     */
    @Test
    fun testXmlError() {
        val url = sampleUrl()
        val dav = DavResource(httpClient, url)

        val body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
                "<D:error xmlns:D=\"DAV:\">\n" +
                "  <D:lock-token-submitted>\n" +
                "    <D:href>/workspace/webdav/</D:href>\n" +
                "  </D:lock-token-submitted>\n" +
                "</D:error>\n"
        mockServer.enqueue(MockResponse()
                .setResponseCode(423)
                .setHeader("Content-Type", "application/xml; charset=\"utf-8\"")
                .setBody(body))
        try {
            dav.propfind(0, ResourceType.NAME).close()
            fail("Expected HttpException")
        } catch (e: HttpException) {
            assertEquals(e.code, 423)
            assertTrue(e.errors.contains(Property.Name(XmlUtils.NS_WEBDAV, "lock-token-submitted")))
            assertEquals(body, e.responseBody)
        }
    }

}
 No newline at end of file
+2 −2
Original line number Diff line number Diff line
@@ -161,7 +161,7 @@ open class DavResource @JvmOverloads constructor(
            properties += GetETag(eTag)
        }

        val body = response.body() ?: throw HttpException("Received GET response without body", response)
        val body = response.body() ?: throw DavException("Received GET response without body", httpResponse = response)
        body.contentType()?.let { mimeType ->
            properties += GetContentType(mimeType)
        }
@@ -393,7 +393,7 @@ open class DavResource @JvmOverloads constructor(
                    log.fine("Redirected, new location = $target")
                    location = target
                } else
                    throw HttpException("Redirected without new Location")
                    throw DavException("Redirected without new Location")
            }
        } finally {
            response.body()?.close()
+146 −2
Original line number Diff line number Diff line
@@ -6,7 +6,151 @@

package at.bitfire.dav4android.exception

import at.bitfire.dav4android.Constants
import at.bitfire.dav4android.Property
import at.bitfire.dav4android.XmlUtils
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.Serializable
import java.util.logging.Level

/**
 * Signals that an error occurred during a WebDAV-related operation.
 *
 * This could be a logical error like when a required ETag has not been
 * received, but also an explicit HTTP error.
 */
open class DavException @JvmOverloads constructor(
        message: String,
        ex: Throwable? = null
): Exception(message, ex)
 No newline at end of file
        ex: Throwable? = null,

        /**
         * An associated HTTP [Response]. Will be closed after evaluation.
         */
        httpResponse: Response? = null
): Exception(message, ex), Serializable {

    companion object {

        const val MAX_EXCERPT_SIZE = 10*1024   // don't dump more than 20 kB

        fun isPlainText(type: MediaType) =
                type.type() == "text" ||
                (type.type() == "application" && type.subtype() in arrayOf("html", "xml"))

    }

    var request: Request? = null
        private set
    var requestBody: String? = null
        private set

    /**
     * Associated HTTP [Response]. Do not access [Response.body] because it will be closed.
     * Use [responseBody] instead.
     */
    val response: Response?

    /**
     * Body excerpt of [response] (up to [MAX_EXCERPT_SIZE] characters). Only available
     * if the HTTP response body was textual content.
     */
    var responseBody: String? = null
        private set

    /**
     * Precondition/postcondition XML elements which have been found in the XML response.
     */
    var errors: Set<Property.Name> = setOf()
        private set


    init {
        if (httpResponse != null) {
            response = httpResponse

            try {
                request = httpResponse.request()

                request?.body()?.let { body ->
                    body.contentType()?.let {
                        if (isPlainText(it)) {
                            val buffer = Buffer()
                            body.writeTo(buffer)
                            val baos = ByteArrayOutputStream()
                            buffer.writeTo(baos)
                            requestBody = baos.toString(it.charset(Charsets.UTF_8)!!.name())
                        }
                    }
                }
            } catch (e: Exception) {
                Constants.log.log(Level.WARNING, "Couldn't read HTTP request", e)
                requestBody = "Couldn't read HTTP request: ${e.message}"
            }

            try {
                // save response body excerpt
                if (httpResponse.body()?.source() != null) {
                    // response body has a source

                    httpResponse.peekBody(MAX_EXCERPT_SIZE.toLong())?.use { body ->
                        body.contentType()?.let {
                            if (isPlainText(it))
                                responseBody = body.string()
                        }
                    }

                    httpResponse.body()?.use { body ->
                        body.contentType()?.let {
                            if (it.type() in arrayOf("application", "text") && it.subtype() == "xml") {
                                // look for precondition/postcondition XML elements
                                try {
                                    val parser = XmlUtils.newPullParser()
                                    parser.setInput(body.charStream())

                                    var eventType = parser.eventType
                                    while (eventType != XmlPullParser.END_DOCUMENT) {
                                        if (eventType == XmlPullParser.START_TAG && parser.depth == 1)
                                            if (parser.namespace == XmlUtils.NS_WEBDAV && parser.name == "error")
                                                errors = parseXmlErrors(parser)
                                        eventType = parser.next()
                                    }
                                } catch (e: XmlPullParserException) {
                                    Constants.log.log(Level.WARNING, "Couldn't parse XML response", e)
                                }
                            }
                        }
                    }
                }
            } catch (e: IOException) {
                Constants.log.log(Level.WARNING, "Couldn't read HTTP response", e)
                responseBody = "Couldn't read HTTP response: ${e.message}"
            } finally {
                httpResponse.body()?.close()
            }
        } else
            response = null
    }


    private fun parseXmlErrors(parser: XmlPullParser): Set<Property.Name> {
        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
    }

}
 No newline at end of file
Loading