From aacfc15cb7b8b1809367ea4fd6678ab41a63d4c3 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 27 Aug 2025 14:39:00 +0200 Subject: [PATCH 1/4] feat: client interceptor Signed-off-by: alperozturk --- .../com/nextcloud/common/NextcloudClient.kt | 8 + .../com/nextcloud/common/OkHttpMethodBase.kt | 12 +- .../android/lib/common/OwnCloudClient.java | 9 +- .../common/interceptor/ClientInterceptor.kt | 202 ++++++++++++++++++ .../android/lib/common/utils/Log_OC.java | 4 + .../responseFormat/ResponseFormatDetector.kt | 10 +- 6 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt diff --git a/library/src/main/java/com/nextcloud/common/NextcloudClient.kt b/library/src/main/java/com/nextcloud/common/NextcloudClient.kt index 830822308f..d06fe34fd6 100644 --- a/library/src/main/java/com/nextcloud/common/NextcloudClient.kt +++ b/library/src/main/java/com/nextcloud/common/NextcloudClient.kt @@ -18,6 +18,7 @@ import com.owncloud.android.lib.common.OwnCloudClientFactory.DEFAULT_CONNECTION_ import com.owncloud.android.lib.common.OwnCloudClientFactory.DEFAULT_DATA_TIMEOUT_LONG import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.interceptor.ClientInterceptor import com.owncloud.android.lib.common.network.AdvancedX509KeyManager import com.owncloud.android.lib.common.network.AdvancedX509TrustManager import com.owncloud.android.lib.common.network.NetworkUtils @@ -43,6 +44,7 @@ class NextcloudClient private constructor( val context: Context ) : NextcloudUriProvider by delegate { var followRedirects = true + private val interceptor = ClientInterceptor() constructor( baseUri: Uri, @@ -126,6 +128,7 @@ class NextcloudClient private constructor( @Throws(IOException::class) fun execute(method: OkHttpMethodBase): Int { + interceptor.interceptOkHttpMethodBaseRequest(method) val httpStatus = method.execute(this) if (httpStatus == HttpStatus.SC_BAD_REQUEST) { val uri = method.uri @@ -137,7 +140,10 @@ class NextcloudClient private constructor( internal fun execute(request: Request): ResponseOrError = try { + interceptor.interceptOkHttp3Request(request) val response = client.newCall(request).execute() + interceptor.interceptOkHttp3Response(response) + if (response.code == HttpStatus.SC_BAD_REQUEST) { val url = request.url Log_OC.e(TAG, "Received http status 400 for $url -> removing client certificate") @@ -150,6 +156,7 @@ class NextcloudClient private constructor( @Throws(IOException::class) fun followRedirection(method: OkHttpMethodBase): RedirectionPath { + interceptor.interceptOkHttpMethodBaseRequest(method) var redirectionsCount = 0 var status = method.getStatusCode() val result = RedirectionPath(status, OwnCloudClient.MAX_REDIRECTIONS_COUNT) @@ -179,6 +186,7 @@ class NextcloudClient private constructor( } status = method.execute(this) + interceptor.interceptOkHttpMethodBaseResponse(method, status) result.addStatus(status) redirectionsCount++ } else { diff --git a/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt b/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt index aba9c23917..ac8ecb89da 100644 --- a/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt +++ b/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt @@ -36,7 +36,7 @@ abstract class OkHttpMethodBase( private var response: Response? = null private var queryMap: Map = HashMap() - private val requestHeaders: MutableMap = HashMap() + val requestHeaders: MutableMap = HashMap() private val requestBuilder: Request.Builder = Request.Builder() private var request: Request? = null @@ -152,6 +152,16 @@ abstract class OkHttpMethodBase( return response?.code ?: UNKNOWN_STATUS_CODE } + fun getRequestBodyAsString(): String = + try { + val copy = request?.newBuilder()?.build() + val buffer = okio.Buffer() + copy?.body?.writeTo(buffer) + buffer.readUtf8() + } catch (_: Exception) { + "" + } + abstract fun applyType(temp: Request.Builder) fun isSuccess(): Boolean = getStatusCode() == HttpURLConnection.HTTP_OK diff --git a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java index 3d10638ee6..b745326bdf 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java +++ b/library/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java @@ -23,6 +23,7 @@ import com.nextcloud.common.DNSCache; import com.nextcloud.common.NextcloudUriDelegate; import com.owncloud.android.lib.common.accounts.AccountUtils; +import com.owncloud.android.lib.common.interceptor.ClientInterceptor; import com.owncloud.android.lib.common.network.AdvancedX509KeyManager; import com.owncloud.android.lib.common.network.RedirectionPath; import com.owncloud.android.lib.common.utils.Log_OC; @@ -66,6 +67,7 @@ public class OwnCloudClient extends HttpClient { private int mInstanceNumber; private AdvancedX509KeyManager keyManager; + private final ClientInterceptor interceptor = new ClientInterceptor(); /** * Constructor @@ -93,7 +95,6 @@ public OwnCloudClient(Uri baseUri, HttpConnectionManager connectionMgr, Context getParams().setParameter(PARAM_SINGLE_COOKIE_HEADER, PARAM_SINGLE_COOKIE_HEADER_VALUE); applyProxySettings(); - clearCredentials(); } @@ -141,6 +142,7 @@ public void clearCredentials() { * @param connectionTimeout Timeout to set for connection establishment */ public int executeMethod(HttpMethodBase method, int readTimeout, int connectionTimeout) throws IOException { + interceptor.interceptHttpMethodBaseRequest(method); int oldSoTimeout = getParams().getSoTimeout(); int oldConnectionTimeout = getHttpConnectionManager().getParams().getConnectionTimeout(); @@ -158,6 +160,9 @@ public int executeMethod(HttpMethodBase method, int readTimeout, int connectionT Log_OC.e(TAG, "Received http status 400 for " + uri + " -> removing client certificate"); keyManager.removeKeys(uri); } + + interceptor.interceptHttpMethodBaseResponse(method, httpStatus); + return httpStatus; } finally { getParams().setSoTimeout(oldSoTimeout); @@ -175,6 +180,7 @@ public int executeMethod(HttpMethodBase method, int readTimeout, int connectionT */ @Override public int executeMethod(HttpMethod method) throws IOException { + interceptor.interceptHttpMethodRequest(method); final String hostname = method.getURI().getHost(); try { @@ -207,6 +213,7 @@ public int executeMethod(HttpMethod method) throws IOException { // logCookiesAtState("after"); // logSetCookiesAtResponse(method.getResponseHeaders()); + interceptor.interceptHttpMethodResponse(method, status); return status; } catch (SocketTimeoutException | ConnectException e) { diff --git a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt new file mode 100644 index 0000000000..7226508f63 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt @@ -0,0 +1,202 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.common.interceptor + +import com.nextcloud.common.OkHttpMethodBase +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.common.utils.responseFormat.ResponseFormat +import com.owncloud.android.lib.common.utils.responseFormat.ResponseFormatDetector +import okhttp3.Request +import okhttp3.Response +import org.apache.commons.httpclient.HttpMethod +import org.apache.commons.httpclient.HttpMethodBase +import org.json.JSONArray +import org.json.JSONObject +import org.w3c.dom.Document +import org.xml.sax.InputSource +import java.io.StringReader +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +class ClientInterceptor { + companion object { + private const val TAG = "ClientInterceptor" + } + + fun interceptHttpMethodBaseRequest(method: HttpMethodBase) { + Log_OC.d(TAG, "➡️ Method: ${method.name} 🌐 URL: ${method.uri}") + logHeaders(method.requestHeaders.map { it.name to it.value }) + + if (method is org.apache.commons.httpclient.methods.EntityEnclosingMethod) { + val buffer = java.io.ByteArrayOutputStream() + method.requestEntity?.writeRequest(buffer) + val body = buffer.toString(method.requestCharSet ?: Charsets.UTF_8.name()) + logBody(body, method.getRequestHeader("Content-Type")?.value, "Request") + } + Log_OC.d(TAG, "-------------------------") + } + + fun interceptHttpMethodBaseResponse( + method: HttpMethodBase, + statusCode: Int + ) { + Log_OC.d(TAG, "⬅️ Status Code: $statusCode") + logHeaders(method.responseHeaders.map { it.name to it.value }) + logBody(method.responseBodyAsString, method.getResponseHeader("Content-Type")?.value, "Response") + Log_OC.d(TAG, "-------------------------") + } + + fun interceptHttpMethodRequest(method: HttpMethod) { + Log_OC.d(TAG, "➡️ Method: ${method.name} 🌐 URL: ${method.uri}") + logHeaders(method.requestHeaders.map { it.name to it.value }) + + if (method is org.apache.commons.httpclient.methods.EntityEnclosingMethod) { + val buffer = java.io.ByteArrayOutputStream() + method.requestEntity?.writeRequest(buffer) + val body = buffer.toString(method.requestCharSet ?: Charsets.UTF_8.name()) + logBody(body, method.getRequestHeader("Content-Type")?.value, "Request") + } + Log_OC.d(TAG, "-------------------------") + } + + fun interceptHttpMethodResponse( + method: HttpMethod, + statusCode: Int + ) { + Log_OC.d(TAG, "⬅️ Status Code: $statusCode") + logHeaders(method.responseHeaders.map { it.name to it.value }) + logBody(method.responseBodyAsString, method.getResponseHeader("Content-Type")?.value, "Response") + Log_OC.d(TAG, "-------------------------") + } + + fun interceptOkHttp3Request(request: Request) { + Log_OC.d(TAG, "➡️ Method: ${request.method} 🌐 URL: ${request.url}") + logHeaders(request.headers.toMultimap().flatMap { (k, vList) -> vList.map { k to it } }) + + request.body?.let { + val buffer = okio.Buffer() + it.writeTo(buffer) + logBody(buffer.readUtf8(), it.contentType()?.toString(), "Request") + } + Log_OC.d(TAG, "-------------------------") + } + + fun interceptOkHttp3Response(response: Response) { + Log_OC.d(TAG, "⬅️ Status: ${response.code}") + logHeaders(response.headers.toMultimap().flatMap { (k, vList) -> vList.map { k to it } }) + logBody(response.peekBody(Long.MAX_VALUE).string(), response.body.contentType()?.toString(), "Response") + Log_OC.d(TAG, "-------------------------") + } + + fun interceptOkHttpMethodBaseRequest(method: OkHttpMethodBase) { + Log_OC.d(TAG, "➡️ Method: ${method.javaClass.simpleName} 🌐 URL: ${method.uri}") + logHeaders(method.requestHeaders.map { it.key to it.value }) + logBody(method.getRequestBodyAsString(), method.getRequestHeader("Content-Type"), "Request") + Log_OC.d(TAG, "-------------------------") + } + + fun interceptOkHttpMethodBaseResponse( + method: OkHttpMethodBase, + statusCode: Int + ) { + Log_OC.d(TAG, "⬅️ Status Code: $statusCode") + logHeaders(method.getResponseHeaders().toMultimap().flatMap { (k, vList) -> vList.map { k to it } }) + logBody(method.getResponseBodyAsString(), method.getResponseHeader("Content-Type"), "Response") + Log_OC.d(TAG, "-------------------------") + } + + // region Private Methods + private fun formatXml( + xml: String, + indent: Int = 2 + ): String = + try { + val builder = + DocumentBuilderFactory + .newInstance() + .apply { + isNamespaceAware = true + isIgnoringComments = true + isIgnoringElementContentWhitespace = true + }.newDocumentBuilder() + + val doc: Document = builder.parse(InputSource(StringReader(xml))) + + val transformer = + TransformerFactory.newInstance().newTransformer().apply { + setOutputProperty(OutputKeys.INDENT, "yes") + setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indent.toString()) + setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no") + setOutputProperty(OutputKeys.ENCODING, "UTF-8") + } + + val writer = java.io.StringWriter() + transformer.transform(DOMSource(doc), StreamResult(writer)) + writer.toString() + } catch (_: Exception) { + xml + } + + private fun formatJson( + json: String, + indent: Int = 2 + ): String = + try { + val trimmed = json.trim() + when { + trimmed.startsWith("{") -> JSONObject(trimmed).toString(indent) + trimmed.startsWith("[") -> JSONArray(trimmed).toString(indent) + else -> json + } + } catch (_: Exception) { + json + } + + private fun formatBody( + body: String, + contentType: String + ): String { + val bodyFormat = ResponseFormatDetector.detectFormat(body) + + return when { + contentType.contains("xml", true) || bodyFormat == ResponseFormat.XML -> formatXml(body) + contentType.contains("json", true) || bodyFormat == ResponseFormat.JSON -> formatJson(body) + else -> body + } + } + + private fun isValidContentType(contentType: String): Boolean = + contentType.contains("application/json") || + contentType.contains("text") || + contentType.contains("xml") || + contentType.isEmpty() + + private fun logHeaders(headers: Iterable>) { + headers.forEach { (name, value) -> Log_OC.d(TAG, "📑 Header: $name: $value") } + } + + @Suppress("TooGenericExceptionCaught") + private fun logBody( + body: String?, + contentType: String?, + label: String + ) { + if (!body.isNullOrBlank() && isValidContentType(contentType ?: "")) { + try { + val formatted = formatBody(body, contentType ?: "") + Log_OC.d(TAG, "📦 $label Body:\n$formatted") + } catch (e: Exception) { + Log_OC.w(TAG, "⚠️ Error reading $label body: $e") + } + } + } + // endregion +} diff --git a/library/src/main/java/com/owncloud/android/lib/common/utils/Log_OC.java b/library/src/main/java/com/owncloud/android/lib/common/utils/Log_OC.java index 7055377559..a18649307a 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/utils/Log_OC.java +++ b/library/src/main/java/com/owncloud/android/lib/common/utils/Log_OC.java @@ -62,6 +62,10 @@ public interface Adapter { void wtf(String tag, String message); } + public static boolean isEnabled() { + return isEnabled; + } + /** * This is legacy logger implementation extracted to allow * the code to compile and run without hiccup. diff --git a/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt b/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt index ed77deb9ad..b1f58e51ab 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt @@ -7,7 +7,6 @@ package com.owncloud.android.lib.common.utils.responseFormat -import com.owncloud.android.lib.common.utils.Log_OC import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -15,8 +14,6 @@ import java.io.ByteArrayInputStream import javax.xml.parsers.DocumentBuilderFactory object ResponseFormatDetector { - private const val TAG = "ResponseFormatDetector" - fun detectFormat(input: String): ResponseFormat = when { isJSON(input) -> ResponseFormat.JSON @@ -28,13 +25,11 @@ object ResponseFormatDetector { try { JSONObject(input) true - } catch (e: JSONException) { + } catch (_: JSONException) { try { - Log_OC.i(TAG, "Info it's not JSONObject: $e") JSONArray(input) true } catch (e: JSONException) { - Log_OC.e(TAG, "Exception it's not JSONArray: $e") false } } @@ -47,8 +42,7 @@ object ResponseFormatDetector { val stream = ByteArrayInputStream(input.toByteArray()) builder.parse(stream) true - } catch (e: Exception) { - Log_OC.e(TAG, "Exception isXML: $e") + } catch (_: Exception) { false } } From 1bd702be00eb2e1819ba7bd62a0e1f524d8f0f28 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 27 Aug 2025 15:04:30 +0200 Subject: [PATCH 2/4] perf: faster format xml Signed-off-by: alperozturk --- .../common/interceptor/ClientInterceptor.kt | 67 ++++++++++--------- .../responseFormat/ResponseFormatDetector.kt | 19 ++---- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt index 7226508f63..e6b3bb31ee 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt @@ -17,11 +17,12 @@ import org.apache.commons.httpclient.HttpMethod import org.apache.commons.httpclient.HttpMethodBase import org.json.JSONArray import org.json.JSONObject -import org.w3c.dom.Document import org.xml.sax.InputSource import java.io.StringReader +import java.io.StringWriter import javax.xml.parsers.DocumentBuilderFactory import javax.xml.transform.OutputKeys +import javax.xml.transform.Transformer import javax.xml.transform.TransformerFactory import javax.xml.transform.dom.DOMSource import javax.xml.transform.stream.StreamResult @@ -114,36 +115,38 @@ class ClientInterceptor { } // region Private Methods - private fun formatXml( - xml: String, - indent: Int = 2 - ): String = - try { - val builder = - DocumentBuilderFactory - .newInstance() - .apply { - isNamespaceAware = true - isIgnoringComments = true - isIgnoringElementContentWhitespace = true - }.newDocumentBuilder() - - val doc: Document = builder.parse(InputSource(StringReader(xml))) - - val transformer = - TransformerFactory.newInstance().newTransformer().apply { - setOutputProperty(OutputKeys.INDENT, "yes") - setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indent.toString()) - setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no") - setOutputProperty(OutputKeys.ENCODING, "UTF-8") - } - - val writer = java.io.StringWriter() - transformer.transform(DOMSource(doc), StreamResult(writer)) - writer.toString() - } catch (_: Exception) { - xml + private val xmlDocBuilder = DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true + isIgnoringComments = true + isIgnoringElementContentWhitespace = true + }.newDocumentBuilder() + + private val threadLocalTransformer = ThreadLocal() + + private fun getTransformer(): Transformer { + var transformer = threadLocalTransformer.get() + if (transformer == null) { + transformer = TransformerFactory.newInstance().newTransformer().apply { + setOutputProperty(OutputKeys.INDENT, "yes") + setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no") + setOutputProperty(OutputKeys.ENCODING, "UTF-8") + } + threadLocalTransformer.set(transformer) } + return transformer + } + + private fun formatXml(xml: String): String = try { + val characterStream = StringReader(xml) + val inputSource = InputSource(characterStream) + val doc = xmlDocBuilder.parse(inputSource) + val writer = StringWriter() + val domSource = DOMSource(doc) + val streamResult = StreamResult(writer) + getTransformer().transform(domSource, streamResult) + writer.toString() + } catch (_: Exception) { xml } private fun formatJson( json: String, @@ -152,8 +155,8 @@ class ClientInterceptor { try { val trimmed = json.trim() when { - trimmed.startsWith("{") -> JSONObject(trimmed).toString(indent) - trimmed.startsWith("[") -> JSONArray(trimmed).toString(indent) + ResponseFormatDetector.isJsonObject(trimmed) -> JSONObject(trimmed).toString(indent) + ResponseFormatDetector.isJsonArray(trimmed) -> JSONArray(trimmed).toString(indent) else -> json } } catch (_: Exception) { diff --git a/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt b/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt index b1f58e51ab..2348ac84a9 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt @@ -8,7 +8,6 @@ package com.owncloud.android.lib.common.utils.responseFormat import org.json.JSONArray -import org.json.JSONException import org.json.JSONObject import java.io.ByteArrayInputStream import javax.xml.parsers.DocumentBuilderFactory @@ -16,23 +15,17 @@ import javax.xml.parsers.DocumentBuilderFactory object ResponseFormatDetector { fun detectFormat(input: String): ResponseFormat = when { - isJSON(input) -> ResponseFormat.JSON isXML(input) -> ResponseFormat.XML + isJSON(input) -> ResponseFormat.JSON else -> ResponseFormat.UNKNOWN } private fun isJSON(input: String): Boolean = - try { - JSONObject(input) - true - } catch (_: JSONException) { - try { - JSONArray(input) - true - } catch (e: JSONException) { - false - } - } + isJsonObject(input) || isJsonArray(input) + + fun isJsonObject(input: String): Boolean = runCatching { JSONObject(input) }.isSuccess + + fun isJsonArray(input: String): Boolean = runCatching { JSONArray(input) }.isSuccess @Suppress("TooGenericExceptionCaught") private fun isXML(input: String): Boolean = From 293a7462dbac84654d1043ad506eae8d4e226237 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 27 Aug 2025 15:05:10 +0200 Subject: [PATCH 3/4] chore: codacy code analytics Signed-off-by: alperozturk --- .../common/interceptor/ClientInterceptor.kt | 50 +++++++++++-------- .../responseFormat/ResponseFormatDetector.kt | 3 +- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt index e6b3bb31ee..816beafc66 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt @@ -27,6 +27,7 @@ import javax.xml.transform.TransformerFactory import javax.xml.transform.dom.DOMSource import javax.xml.transform.stream.StreamResult +@Suppress("TooManyFunctions") class ClientInterceptor { companion object { private const val TAG = "ClientInterceptor" @@ -115,38 +116,45 @@ class ClientInterceptor { } // region Private Methods - private val xmlDocBuilder = DocumentBuilderFactory.newInstance().apply { - isNamespaceAware = true - isIgnoringComments = true - isIgnoringElementContentWhitespace = true - }.newDocumentBuilder() + private val xmlDocBuilder = + DocumentBuilderFactory + .newInstance() + .apply { + isNamespaceAware = true + isIgnoringComments = true + isIgnoringElementContentWhitespace = true + }.newDocumentBuilder() private val threadLocalTransformer = ThreadLocal() private fun getTransformer(): Transformer { var transformer = threadLocalTransformer.get() if (transformer == null) { - transformer = TransformerFactory.newInstance().newTransformer().apply { - setOutputProperty(OutputKeys.INDENT, "yes") - setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") - setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no") - setOutputProperty(OutputKeys.ENCODING, "UTF-8") - } + transformer = + TransformerFactory.newInstance().newTransformer().apply { + setOutputProperty(OutputKeys.INDENT, "yes") + setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no") + setOutputProperty(OutputKeys.ENCODING, "UTF-8") + } threadLocalTransformer.set(transformer) } return transformer } - private fun formatXml(xml: String): String = try { - val characterStream = StringReader(xml) - val inputSource = InputSource(characterStream) - val doc = xmlDocBuilder.parse(inputSource) - val writer = StringWriter() - val domSource = DOMSource(doc) - val streamResult = StreamResult(writer) - getTransformer().transform(domSource, streamResult) - writer.toString() - } catch (_: Exception) { xml } + private fun formatXml(xml: String): String = + try { + val characterStream = StringReader(xml) + val inputSource = InputSource(characterStream) + val doc = xmlDocBuilder.parse(inputSource) + val writer = StringWriter() + val domSource = DOMSource(doc) + val streamResult = StreamResult(writer) + getTransformer().transform(domSource, streamResult) + writer.toString() + } catch (_: Exception) { + xml + } private fun formatJson( json: String, diff --git a/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt b/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt index 2348ac84a9..382959c078 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/utils/responseFormat/ResponseFormatDetector.kt @@ -20,8 +20,7 @@ object ResponseFormatDetector { else -> ResponseFormat.UNKNOWN } - private fun isJSON(input: String): Boolean = - isJsonObject(input) || isJsonArray(input) + private fun isJSON(input: String): Boolean = isJsonObject(input) || isJsonArray(input) fun isJsonObject(input: String): Boolean = runCatching { JSONObject(input) }.isSuccess From 89c7e89c3ff0a913eebf436437c2f9b2d362b168 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 27 Aug 2025 15:14:32 +0200 Subject: [PATCH 4/4] fix: nextcloud client tests Signed-off-by: alperozturk --- .../common/interceptor/ClientInterceptor.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt index 816beafc66..d8035f12f4 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/interceptor/ClientInterceptor.kt @@ -26,6 +26,8 @@ import javax.xml.transform.Transformer import javax.xml.transform.TransformerFactory import javax.xml.transform.dom.DOMSource import javax.xml.transform.stream.StreamResult +import kotlin.collections.component1 +import kotlin.collections.component2 @Suppress("TooManyFunctions") class ClientInterceptor { @@ -81,8 +83,9 @@ class ClientInterceptor { fun interceptOkHttp3Request(request: Request) { Log_OC.d(TAG, "➡️ Method: ${request.method} 🌐 URL: ${request.url}") - logHeaders(request.headers.toMultimap().flatMap { (k, vList) -> vList.map { k to it } }) - + request.headers?.toMultimap()?.let { headerMap -> + logHeaders(headerMap.flatMap { (k, vList) -> vList.map { k to it } }) + } request.body?.let { val buffer = okio.Buffer() it.writeTo(buffer) @@ -93,8 +96,21 @@ class ClientInterceptor { fun interceptOkHttp3Response(response: Response) { Log_OC.d(TAG, "⬅️ Status: ${response.code}") - logHeaders(response.headers.toMultimap().flatMap { (k, vList) -> vList.map { k to it } }) - logBody(response.peekBody(Long.MAX_VALUE).string(), response.body.contentType()?.toString(), "Response") + response.headers?.toMultimap()?.let { headerMap -> + logHeaders(headerMap.flatMap { (k, vList) -> vList.map { k to it } }) + } + + val body = + try { + response.peekBody(Long.MAX_VALUE) + } catch (_: Exception) { + null + } + + body?.string()?.let { bodyString -> + logBody(bodyString, response.body?.contentType()?.toString(), "Response") + } + Log_OC.d(TAG, "-------------------------") }