mirror of https://github.com/M66B/FairEmail.git
parent
1da91952f6
commit
084d0e1536
@ -0,0 +1,175 @@
|
|||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import android.net.TrafficStats
|
||||||
|
import com.bugsnag.android.internal.JsonHelper
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
||||||
|
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
|
||||||
|
import java.net.HttpURLConnection.HTTP_OK
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
internal class DefaultDelivery(
|
||||||
|
private val connectivity: Connectivity?,
|
||||||
|
private val apiKey: String,
|
||||||
|
private val maxStringValueLength: Int,
|
||||||
|
private val logger: Logger
|
||||||
|
) : Delivery {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// 1MB with some fiddle room in case of encoding overhead
|
||||||
|
const val maxPayloadSize = 999700
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||||
|
val status = deliver(
|
||||||
|
deliveryParams.endpoint,
|
||||||
|
JsonHelper.serialize(payload),
|
||||||
|
deliveryParams.headers
|
||||||
|
)
|
||||||
|
logger.i("Session API request finished with status $status")
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun serializePayload(payload: EventPayload): ByteArray {
|
||||||
|
var json = JsonHelper.serialize(payload)
|
||||||
|
if (json.size <= maxPayloadSize) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = payload.event
|
||||||
|
if (event == null) {
|
||||||
|
event = MarshalledEventSource(payload.eventFile!!, apiKey, logger).invoke()
|
||||||
|
payload.event = event
|
||||||
|
payload.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(maxStringValueLength)
|
||||||
|
event.impl.internalMetrics.setMetadataTrimMetrics(
|
||||||
|
itemsTrimmed,
|
||||||
|
dataTrimmed
|
||||||
|
)
|
||||||
|
|
||||||
|
json = JsonHelper.serialize(payload)
|
||||||
|
if (json.size <= maxPayloadSize) {
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
val breadcrumbAndBytesRemovedCounts =
|
||||||
|
event.impl.trimBreadcrumbsBy(json.size - maxPayloadSize)
|
||||||
|
event.impl.internalMetrics.setBreadcrumbTrimMetrics(
|
||||||
|
breadcrumbAndBytesRemovedCounts.itemsTrimmed,
|
||||||
|
breadcrumbAndBytesRemovedCounts.dataTrimmed
|
||||||
|
)
|
||||||
|
return JsonHelper.serialize(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||||
|
val json = serializePayload(payload)
|
||||||
|
val status = deliver(deliveryParams.endpoint, json, deliveryParams.headers)
|
||||||
|
logger.i("Error API request finished with status $status")
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deliver(
|
||||||
|
urlString: String,
|
||||||
|
json: ByteArray,
|
||||||
|
headers: Map<String, String?>
|
||||||
|
): DeliveryStatus {
|
||||||
|
|
||||||
|
TrafficStats.setThreadStatsTag(1)
|
||||||
|
if (connectivity != null && !connectivity.hasNetworkConnection()) {
|
||||||
|
return DeliveryStatus.UNDELIVERED
|
||||||
|
}
|
||||||
|
var conn: HttpURLConnection? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn = makeRequest(URL(urlString), json, headers)
|
||||||
|
|
||||||
|
// End the request, get the response code
|
||||||
|
val responseCode = conn.responseCode
|
||||||
|
val status = getDeliveryStatus(responseCode)
|
||||||
|
logRequestInfo(responseCode, conn, status)
|
||||||
|
return status
|
||||||
|
} catch (oom: OutOfMemoryError) {
|
||||||
|
// attempt to persist the payload on disk. This approach uses streams to write to a
|
||||||
|
// file, which takes less memory than serializing the payload into a ByteArray, and
|
||||||
|
// therefore has a reasonable chance of retaining the payload for future delivery.
|
||||||
|
logger.w("Encountered OOM delivering payload, falling back to persist on disk", oom)
|
||||||
|
return DeliveryStatus.UNDELIVERED
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
logger.w("IOException encountered in request", exception)
|
||||||
|
return DeliveryStatus.UNDELIVERED
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
logger.w("Unexpected error delivering payload", exception)
|
||||||
|
return DeliveryStatus.FAILURE
|
||||||
|
} finally {
|
||||||
|
conn?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeRequest(
|
||||||
|
url: URL,
|
||||||
|
json: ByteArray,
|
||||||
|
headers: Map<String, String?>
|
||||||
|
): HttpURLConnection {
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.doOutput = true
|
||||||
|
|
||||||
|
// avoids creating a buffer within HttpUrlConnection, see
|
||||||
|
// https://developer.android.com/reference/java/net/HttpURLConnection
|
||||||
|
conn.setFixedLengthStreamingMode(json.size)
|
||||||
|
|
||||||
|
// calculate the SHA-1 digest and add all other headers
|
||||||
|
computeSha1Digest(json)?.let { digest ->
|
||||||
|
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
|
||||||
|
}
|
||||||
|
headers.forEach { (key, value) ->
|
||||||
|
if (value != null) {
|
||||||
|
conn.addRequestProperty(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the JSON payload
|
||||||
|
conn.outputStream.use {
|
||||||
|
it.write(json)
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) {
|
||||||
|
runCatching {
|
||||||
|
logger.i(
|
||||||
|
"Request completed with code $code, " +
|
||||||
|
"message: ${conn.responseMessage}, " +
|
||||||
|
"headers: ${conn.headerFields}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
conn.inputStream.bufferedReader().use {
|
||||||
|
logger.d("Received request response: ${it.readText()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
if (status != DeliveryStatus.DELIVERED) {
|
||||||
|
conn.errorStream.bufferedReader().use {
|
||||||
|
logger.w("Request error details: ${it.readText()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getDeliveryStatus(responseCode: Int): DeliveryStatus {
|
||||||
|
return when {
|
||||||
|
responseCode in HTTP_OK..299 -> DeliveryStatus.DELIVERED
|
||||||
|
isUnrecoverableStatusCode(responseCode) -> DeliveryStatus.FAILURE
|
||||||
|
else -> DeliveryStatus.UNDELIVERED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUnrecoverableStatusCode(responseCode: Int) =
|
||||||
|
responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable
|
||||||
|
responseCode != HTTP_CLIENT_TIMEOUT && // except for 408
|
||||||
|
responseCode != 429 // and 429
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package com.bugsnag.android
|
||||||
|
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the NDK plugin if it is loaded, otherwise does nothing / returns the default.
|
||||||
|
*/
|
||||||
|
internal object NdkPluginCaller {
|
||||||
|
private var ndkPlugin: Plugin? = null
|
||||||
|
private var setInternalMetricsEnabled: Method? = null
|
||||||
|
private var setStaticData: Method? = null
|
||||||
|
private var getSignalUnwindStackFunction: Method? = null
|
||||||
|
private var getCurrentCallbackSetCounts: Method? = null
|
||||||
|
private var getCurrentNativeApiCallUsage: Method? = null
|
||||||
|
private var initCallbackCounts: Method? = null
|
||||||
|
private var notifyAddCallback: Method? = null
|
||||||
|
private var notifyRemoveCallback: Method? = null
|
||||||
|
|
||||||
|
private fun getMethod(name: String, vararg parameterTypes: Class<*>): Method? {
|
||||||
|
val plugin = ndkPlugin
|
||||||
|
if (plugin == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return plugin.javaClass.getMethod(name, *parameterTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNdkPlugin(plugin: Plugin?) {
|
||||||
|
if (plugin != null) {
|
||||||
|
ndkPlugin = plugin
|
||||||
|
setInternalMetricsEnabled = getMethod("setInternalMetricsEnabled", Boolean::class.java)
|
||||||
|
setStaticData = getMethod("setStaticData", Map::class.java)
|
||||||
|
getSignalUnwindStackFunction = getMethod("getSignalUnwindStackFunction")
|
||||||
|
getCurrentCallbackSetCounts = getMethod("getCurrentCallbackSetCounts")
|
||||||
|
getCurrentNativeApiCallUsage = getMethod("getCurrentNativeApiCallUsage")
|
||||||
|
initCallbackCounts = getMethod("initCallbackCounts", Map::class.java)
|
||||||
|
notifyAddCallback = getMethod("notifyAddCallback", String::class.java)
|
||||||
|
notifyRemoveCallback = getMethod("notifyRemoveCallback", String::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSignalUnwindStackFunction(): Long {
|
||||||
|
val method = getSignalUnwindStackFunction
|
||||||
|
if (method != null) {
|
||||||
|
return method.invoke(ndkPlugin) as Long
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInternalMetricsEnabled(enabled: Boolean) {
|
||||||
|
val method = setInternalMetricsEnabled
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentCallbackSetCounts(): Map<String, Int>? {
|
||||||
|
val method = getCurrentCallbackSetCounts
|
||||||
|
if (method != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return method.invoke(ndkPlugin) as Map<String, Int>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentNativeApiCallUsage(): Map<String, Boolean>? {
|
||||||
|
val method = getCurrentNativeApiCallUsage
|
||||||
|
if (method != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return method.invoke(ndkPlugin) as Map<String, Boolean>
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initCallbackCounts(counts: Map<String, Int>) {
|
||||||
|
val method = initCallbackCounts
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, counts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyAddCallback(callback: String) {
|
||||||
|
val method = notifyAddCallback
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyRemoveCallback(callback: String) {
|
||||||
|
val method = notifyRemoveCallback
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStaticData(data: Map<String, Any>) {
|
||||||
|
val method = setStaticData
|
||||||
|
if (method != null) {
|
||||||
|
method.invoke(ndkPlugin, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores internal metrics for Bugsnag use.
|
||||||
|
*/
|
||||||
|
interface InternalMetrics {
|
||||||
|
/**
|
||||||
|
* Returns a map that can be merged with the top-level JSON report.
|
||||||
|
*/
|
||||||
|
fun toJsonableMap(): Map<String, Any>
|
||||||
|
|
||||||
|
fun setConfigDifferences(differences: Map<String, Any>)
|
||||||
|
|
||||||
|
fun setCallbackCounts(newCallbackCounts: Map<String, Int>)
|
||||||
|
|
||||||
|
fun notifyAddCallback(callback: String)
|
||||||
|
|
||||||
|
fun notifyRemoveCallback(callback: String)
|
||||||
|
|
||||||
|
fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int)
|
||||||
|
|
||||||
|
fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class TrimMetrics(
|
||||||
|
val itemsTrimmed: Int, // breadcrumbs, strings, whatever
|
||||||
|
val dataTrimmed: Int // chars, bytes, whatever
|
||||||
|
)
|
@ -0,0 +1,111 @@
|
|||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import com.bugsnag.android.NdkPluginCaller
|
||||||
|
|
||||||
|
class InternalMetricsImpl(source: Map<String, Any>? = null) : InternalMetrics {
|
||||||
|
private val configDifferences: MutableMap<String, Any>
|
||||||
|
private val callbackCounts: MutableMap<String, Int>
|
||||||
|
private var metadataStringsTrimmedCount = 0
|
||||||
|
private var metadataCharsTruncatedCount = 0
|
||||||
|
private var breadcrumbsRemovedCount = 0
|
||||||
|
private var breadcrumbBytesRemovedCount = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (source != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
configDifferences = (source["config"] as MutableMap<String, Any>?) ?: hashMapOf()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
callbackCounts = (source["callbacks"] as MutableMap<String, Int>?) ?: hashMapOf()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val system = source["system"] as MutableMap<String, Any>?
|
||||||
|
if (system != null) {
|
||||||
|
metadataStringsTrimmedCount = (system["stringsTruncated"] as Number?)?.toInt() ?: 0
|
||||||
|
metadataCharsTruncatedCount = (system["stringCharsTruncated"] as Number?)?.toInt() ?: 0
|
||||||
|
breadcrumbsRemovedCount = (system["breadcrumbsRemovedCount"] as Number?)?.toInt() ?: 0
|
||||||
|
breadcrumbBytesRemovedCount = (system["breadcrumbBytesRemoved"] as Number?)?.toInt() ?: 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
configDifferences = hashMapOf()
|
||||||
|
callbackCounts = hashMapOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJsonableMap(): Map<String, Any> {
|
||||||
|
val callbacks = allCallbacks()
|
||||||
|
|
||||||
|
val system = listOfNotNull(
|
||||||
|
if (metadataStringsTrimmedCount > 0) "stringsTruncated" to metadataStringsTrimmedCount else null,
|
||||||
|
if (metadataCharsTruncatedCount > 0) "stringCharsTruncated" to metadataCharsTruncatedCount else null,
|
||||||
|
if (breadcrumbsRemovedCount > 0) "breadcrumbsRemoved" to breadcrumbsRemovedCount else null,
|
||||||
|
if (breadcrumbBytesRemovedCount > 0) "breadcrumbBytesRemoved" to breadcrumbBytesRemovedCount else null,
|
||||||
|
).toMap()
|
||||||
|
|
||||||
|
return listOfNotNull(
|
||||||
|
if (configDifferences.isNotEmpty()) "config" to configDifferences else null,
|
||||||
|
if (callbacks.isNotEmpty()) "callbacks" to callbacks else null,
|
||||||
|
if (system.isNotEmpty()) "system" to system else null,
|
||||||
|
).toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setConfigDifferences(differences: Map<String, Any>) {
|
||||||
|
configDifferences.clear()
|
||||||
|
configDifferences.putAll(differences)
|
||||||
|
// This is currently the only place where we set static data.
|
||||||
|
// When that changes in future, we'll need a StaticData object to properly merge data
|
||||||
|
// coming from multiple sources.
|
||||||
|
NdkPluginCaller.setStaticData(mapOf("usage" to mapOf("config" to configDifferences)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) {
|
||||||
|
callbackCounts.clear()
|
||||||
|
callbackCounts.putAll(newCallbackCounts)
|
||||||
|
NdkPluginCaller.initCallbackCounts(newCallbackCounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyAddCallback(callback: String) {
|
||||||
|
modifyCallback(callback, 1)
|
||||||
|
NdkPluginCaller.notifyAddCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyRemoveCallback(callback: String) {
|
||||||
|
modifyCallback(callback, -1)
|
||||||
|
NdkPluginCaller.notifyRemoveCallback(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun modifyCallback(callback: String, delta: Int) {
|
||||||
|
var currentValue = callbackCounts[callback] ?: 0
|
||||||
|
currentValue += delta
|
||||||
|
callbackCounts[callback] = currentValue.coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun allCallbacks(): Map<String, Any> {
|
||||||
|
val result = hashMapOf<String, Any>()
|
||||||
|
result.putAll(callbackCounts)
|
||||||
|
|
||||||
|
val counts = NdkPluginCaller.getCurrentCallbackSetCounts()
|
||||||
|
if (counts != null) {
|
||||||
|
// ndkOnError comes from the native side. The rest we already have.
|
||||||
|
val ndkOnError = counts["ndkOnError"]
|
||||||
|
if (ndkOnError != null) {
|
||||||
|
result["ndkOnError"] = ndkOnError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val usage = NdkPluginCaller.getCurrentNativeApiCallUsage()
|
||||||
|
if (usage != null) {
|
||||||
|
result.putAll(usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) {
|
||||||
|
metadataStringsTrimmedCount = stringsTrimmed
|
||||||
|
metadataCharsTruncatedCount = charsRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) {
|
||||||
|
breadcrumbsRemovedCount = breadcrumbsRemoved
|
||||||
|
breadcrumbBytesRemovedCount = bytesRemoved
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
class InternalMetricsNoop : InternalMetrics {
|
||||||
|
override fun toJsonableMap(): Map<String, Any> = emptyMap()
|
||||||
|
override fun setConfigDifferences(differences: Map<String, Any>) = Unit
|
||||||
|
override fun setCallbackCounts(newCallbackCounts: Map<String, Int>) = Unit
|
||||||
|
override fun notifyAddCallback(callback: String) = Unit
|
||||||
|
override fun notifyRemoveCallback(callback: String) = Unit
|
||||||
|
override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) = Unit
|
||||||
|
override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) = Unit
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
package com.bugsnag.android.internal
|
||||||
|
|
||||||
|
import java.util.EnumMap
|
||||||
|
import java.util.Hashtable
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.TreeMap
|
||||||
|
import java.util.Vector
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
import java.util.concurrent.ConcurrentMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
internal object StringUtils {
|
||||||
|
private const val trimMessageLength = "***<9> CHARS TRUNCATED***".length
|
||||||
|
|
||||||
|
fun stringTrimmedTo(maxLength: Int, str: String): String {
|
||||||
|
val excessCharCount = str.length - maxLength
|
||||||
|
return when {
|
||||||
|
excessCharCount < trimMessageLength -> str
|
||||||
|
else -> "${str.substring(0, maxLength)}***<$excessCharCount> CHARS TRUNCATED***"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
fun trimStringValuesTo(maxStringLength: Int, list: MutableList<Any?>): TrimMetrics {
|
||||||
|
var stringCount = 0
|
||||||
|
var charCount = 0
|
||||||
|
|
||||||
|
repeat(list.size) { index ->
|
||||||
|
trimValue(maxStringLength, list[index]) { newValue, stringTrimmed, charsTrimmed ->
|
||||||
|
list[index] = newValue
|
||||||
|
stringCount += stringTrimmed
|
||||||
|
charCount += charsTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrimMetrics(stringCount, charCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
fun trimStringValuesTo(maxStringLength: Int, map: MutableMap<String, Any?>): TrimMetrics {
|
||||||
|
var stringCount = 0
|
||||||
|
var charCount = 0
|
||||||
|
map.entries.forEach { entry ->
|
||||||
|
trimValue(maxStringLength, entry.value) { newValue, stringTrimmed, charsTrimmed ->
|
||||||
|
entry.setValue(newValue)
|
||||||
|
stringCount += stringTrimmed
|
||||||
|
charCount += charsTrimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrimMetrics(stringCount, charCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unchecked_cast")
|
||||||
|
private inline fun trimValue(
|
||||||
|
maxStringLength: Int,
|
||||||
|
value: Any?,
|
||||||
|
update: (newValue: Any, stringTrimmed: Int, charsTrimmed: Int) -> Unit
|
||||||
|
) {
|
||||||
|
if (value is String && value.length > maxStringLength) {
|
||||||
|
update(stringTrimmedTo(maxStringLength, value), 1, value.length - maxStringLength)
|
||||||
|
} else if (value.isDefinitelyMutableMap()) {
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(
|
||||||
|
maxStringLength,
|
||||||
|
value as MutableMap<String, Any?>
|
||||||
|
)
|
||||||
|
|
||||||
|
update(value, innerStringCount, innerCharCount)
|
||||||
|
} else if (value.isDefinitelyMutableList()) {
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(
|
||||||
|
maxStringLength,
|
||||||
|
value as MutableList<Any?>
|
||||||
|
)
|
||||||
|
|
||||||
|
update(value, innerStringCount, innerCharCount)
|
||||||
|
} else if (value is Map<*, *>) {
|
||||||
|
val newValue = value.toMutableMap() as MutableMap<String, Any?>
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(maxStringLength, newValue)
|
||||||
|
update(newValue, innerStringCount, innerCharCount)
|
||||||
|
} else if (value is Collection<*>) {
|
||||||
|
val newValue = value.toMutableList()
|
||||||
|
val (innerStringCount, innerCharCount) = trimStringValuesTo(maxStringLength, newValue)
|
||||||
|
update(newValue, innerStringCount, innerCharCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to avoid surprises we have a small list of commonly used Map types that are known
|
||||||
|
* to be mutable (avoiding issues around Kotlin trying to determine whether
|
||||||
|
* `Collections.singletonMap` (and such) is mutable or not).
|
||||||
|
*
|
||||||
|
* It is technically possible that a HashMap was extended to be immutable, but it's unlikely.
|
||||||
|
*/
|
||||||
|
private fun Any?.isDefinitelyMutableMap() =
|
||||||
|
this is HashMap<*, *> ||
|
||||||
|
this is TreeMap<*, *> ||
|
||||||
|
this is ConcurrentMap<*, *> || // concurrent automatically implies mutability
|
||||||
|
this is EnumMap<*, *> ||
|
||||||
|
this is Hashtable<*, *> ||
|
||||||
|
this is WeakHashMap<*, *>
|
||||||
|
|
||||||
|
private fun Any?.isDefinitelyMutableList() =
|
||||||
|
this is ArrayList<*> ||
|
||||||
|
this is LinkedList<*> ||
|
||||||
|
this is CopyOnWriteArrayList<*> ||
|
||||||
|
this is Vector<*>
|
||||||
|
}
|
Loading…
Reference in new issue