All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.realm.kotlin.mongodb.AppConfiguration.kt Maven / Gradle / Ivy

Go to download

Sync Library code for Realm Kotlin. This artifact is not supposed to be consumed directly, but through 'io.realm.kotlin:gradle-plugin:1.6.2' instead.

There is a newer version: 2.3.0
Show newest version
/*
 * Copyright 2021 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.realm.kotlin.mongodb

import io.ktor.client.plugins.logging.Logger
import io.realm.kotlin.LogConfiguration
import io.realm.kotlin.Realm
import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi
import io.realm.kotlin.internal.ContextLogger
import io.realm.kotlin.internal.interop.sync.MetadataMode
import io.realm.kotlin.internal.interop.sync.NetworkTransport
import io.realm.kotlin.internal.interop.sync.WebSocketTransport
import io.realm.kotlin.internal.platform.appFilesDirectory
import io.realm.kotlin.internal.platform.canWrite
import io.realm.kotlin.internal.platform.directoryExists
import io.realm.kotlin.internal.platform.fileExists
import io.realm.kotlin.internal.platform.prepareRealmDirectoryPath
import io.realm.kotlin.internal.util.CoroutineDispatcherFactory
import io.realm.kotlin.internal.util.DispatcherHolder
import io.realm.kotlin.internal.util.Validation
import io.realm.kotlin.log.LogLevel
import io.realm.kotlin.log.RealmLog
import io.realm.kotlin.log.RealmLogger
import io.realm.kotlin.mongodb.ext.customData
import io.realm.kotlin.mongodb.ext.profile
import io.realm.kotlin.mongodb.internal.AppConfigurationImpl
import io.realm.kotlin.mongodb.internal.KtorNetworkTransport
import io.realm.kotlin.mongodb.internal.LogObfuscatorImpl
import io.realm.kotlin.mongodb.internal.RealmWebSocketTransport
import io.realm.kotlin.mongodb.sync.SyncConfiguration
import io.realm.kotlin.mongodb.sync.SyncTimeoutOptions
import io.realm.kotlin.mongodb.sync.SyncTimeoutOptionsBuilder
import kotlinx.coroutines.CoroutineDispatcher
import org.mongodb.kbson.ExperimentalKBsonSerializerApi
import org.mongodb.kbson.serialization.EJson

/**
 * An **AppConfiguration** is used to setup linkage to an Atlas App Services Application.
 *
 * Instances of an AppConfiguration can be created by using the [AppConfiguration.Builder] and
 * calling its [AppConfiguration.Builder.build] method or by using [AppConfiguration.create].
 */
public interface AppConfiguration {

    public val appId: String

    // TODO Consider replacing with URL type, but didn't want to include io.ktor.http.Url as it
    //  requires ktor as api dependency
    public val baseUrl: String
    public val encryptionKey: ByteArray?
    public val metadataMode: MetadataMode
    public val syncRootDirectory: String

    /**
     * Authorization header name used for Atlas App services requests.
     */
    public val authorizationHeaderName: String

    /**
     * Custom configured headers that will be sent alongside other headers when
     * making network requests towards Atlas App services.
     */
    public val customRequestHeaders: Map

    /**
     * The name of app. This is only used for debugging.
     *
     * @see [AppConfiguration.Builder.appName]
     */
    public val appName: String?

    /**
     * The version of the app. This is only used for debugging.
     *
     * @see [AppConfiguration.Builder.appVersion]
     */
    public val appVersion: String?

    /**
     * The default EJson decoder that would be used to encode and decode arguments and results
     * when calling remote App [Functions], authenticating with a [customFunction], and retrieving
     * a user [profile] or [customData].
     *
     * It can be set with [Builder.ejson] if a certain configuration, such as contextual classes, is
     * required.
     */
    @OptIn(ExperimentalKBsonSerializerApi::class)
    public val ejson: EJson

    /**
     * The configured [HttpLogObfuscator] for this app. If this property returns `null` no
     * obfuscator is being used.
     */
    public val httpLogObfuscator: HttpLogObfuscator?

    /**
     * If enabled, a single connection is used for all Realms opened
     * with a single sync user. If disabled, a separate connection is used for each
     * Realm.
     *
     * Session multiplexing reduces resources used and typically improves
     * performance. When multiplexing is enabled, the connection is not immediately
     * closed when the last session is closed, but remains open for
     * [SyncTimeoutOptions.connectionLingerTime] defined in [syncTimeoutOptions].
     */
    public val enableSessionMultiplexing: Boolean

    /**
     * The configured timeouts for various aspects of the sync connection from realms.
     */
    public val syncTimeoutOptions: SyncTimeoutOptions

    public companion object {
        /**
         * The default url for App Services applications.
         *
         * @see Builder#baseUrl(String)
         */
        public const val DEFAULT_BASE_URL: String = "https://realm.mongodb.com"

        /**
         * The default header name used to carry authorization data when making network requests
         * towards App Services.
         */
        public const val DEFAULT_AUTHORIZATION_HEADER_NAME: String = "Authorization"

        /**
         * Creates an app configuration with a given [appId] with default values for all
         * optional configuration parameters.
         *
         * @param appId the application id of the App Services Application.
         */
        public fun create(appId: String): AppConfiguration = AppConfiguration.Builder(appId).build()
    }

    /**
     * Builder used to construct instances of an [AppConfiguration] in a fluent manner.
     *
     * @param appId the application id of the App Services Application.
     */
    public class Builder(
        private val appId: String,
    ) {

        private var baseUrl: String = DEFAULT_BASE_URL
        private var dispatcher: CoroutineDispatcher? = null
        private var encryptionKey: ByteArray? = null
        private var logLevel: LogLevel? = null
        private var syncRootDirectory: String = appFilesDirectory()
        private var userLoggers: List = listOf()
        private var networkTransport: NetworkTransport? = null
        private var websocketTransport: WebSocketTransport? = null
        private var appName: String? = null
        private var appVersion: String? = null

        @OptIn(ExperimentalKBsonSerializerApi::class)
        private var ejson: EJson = EJson
        private var httpLogObfuscator: HttpLogObfuscator? = LogObfuscatorImpl
        private val customRequestHeaders = mutableMapOf()
        private var authorizationHeaderName: String = DEFAULT_AUTHORIZATION_HEADER_NAME
        private var enableSessionMultiplexing: Boolean = false
        private var syncTimeoutOptions: SyncTimeoutOptions = SyncTimeoutOptionsBuilder().build()
        private var usePlatformNetworking: Boolean = false

        /**
         * Sets the encryption key used to encrypt the user metadata Realm only. Individual
         * Realms need to use [SyncConfiguration.Builder.encryptionKey] to encrypt them.
         *
         * @param key a 64 byte encryption key.
         * @return the Builder instance used.
         * @throws IllegalArgumentException if the key is not 64 bytes long.
         */
        public fun encryptionKey(key: ByteArray): Builder = apply {
            if (key.size != Realm.ENCRYPTION_KEY_LENGTH) {
                throw IllegalArgumentException("The provided key must be ${Realm.ENCRYPTION_KEY_LENGTH} bytes. Yours was: ${key.size}.")
            }

            this.encryptionKey = key.copyOf()
        }

        /**
         * Sets the base url for the App Services Application. The default value is
         * [DEFAULT_BASE_URL].
         *
         * @param baseUrl the base url for the App Services Application.
         * @return the Builder instance used.
         */
        public fun baseUrl(baseUrl: String): Builder = apply { this.baseUrl = baseUrl }

        /**
         * The dispatcher used to execute internal tasks; most notably remote HTTP requests.
         *
         * @return the Builder instance used.
         */
        public fun dispatcher(dispatcher: CoroutineDispatcher): Builder = apply {
            this.dispatcher = dispatcher
        }

        /**
         * Configures how Realm will report log events for this App.
         *
         * @param level all events at this level or higher will be reported.
         * @param customLoggers any custom loggers to send log events to. A default system logger is
         * installed by default that will redirect to the common logging framework on the platform, i.e.
         * LogCat on Android and NSLog on iOS.
         * @return the Builder instance used.
         */
        @Deprecated("Use io.realm.kotlin.log.RealmLog instead.")
        public fun log(
            level: LogLevel = LogLevel.WARN,
            customLoggers: List = emptyList(),
        ): Builder =
            apply {
                this.logLevel = level
                this.userLoggers = customLoggers
            }

        /**
         * Configures the root folder that marks the location of a `mongodb-realm` folder. This
         * folder contains all files and realms used when synchronizing data between the device and
         * Atlas using Device Sync.
         *
         * The default root directory is platform-dependent:
         * ```
         * // For Android the default directory is obtained using
         * val dir = "${Context.getFilesDir()}"
         *
         * // For JVM platforms the default directory is obtained using
         * val dir = "${System.getProperty("user.dir")}"
         *
         * // For macOS the default directory is obtained using
         * val dir = "${NSFileManager.defaultManager.currentDirectoryPath}"
         *
         * // For iOS the default directory is obtained using
         * val dir = "${NSFileManager.defaultManager.URLForDirectory(
         *      NSDocumentDirectory,
         *      NSUserDomainMask,
         *      null,
         *      true,
         *      null
         * )}"
         * ```
         *
         * @param rootDir the directory where a `mongodb-realm` directory will be created.
         * @return the Builder instance used.
         */
        public fun syncRootDirectory(rootDir: String): Builder = apply {
            val directoryExists = directoryExists(rootDir)
            if (!directoryExists && fileExists(rootDir)) {
                throw IllegalArgumentException("'rootDir' is a file, not a directory: $rootDir.")
            }
            if (!directoryExists) {
                prepareRealmDirectoryPath(rootDir)
            }
            if (!canWrite(rootDir)) {
                throw IllegalArgumentException("Realm directory is not writable: $rootDir.")
            }
            this.syncRootDirectory = rootDir
        }

        /**
         * Sets the debug app name which is added to debug headers for App Services network
         * requests. The default is `null`.
         *
         * @param appName app name used to identify the application.
         * @throws IllegalArgumentException if an empty [appName] is provided.
         * @return the Builder instance used.
         */
        public fun appName(appName: String): Builder = apply {
            Validation.checkEmpty(appName, "appName")
            this.appName = appName
        }

        /**
         * Sets the debug app version which is added to debug headers for App Services network
         * requests. The default is `null`
         *
         * @param appVersion app version used to identify the application.
         * @throws IllegalArgumentException if an empty [appVersion] is provided.
         * @return the Builder instance used.
         */
        public fun appVersion(appVersion: String): Builder = apply {
            Validation.checkEmpty(appVersion, "appVersion")
            this.appVersion = appVersion
        }

        /**
         * Sets the a [HttpLogObfuscator] used to keep sensitive information in HTTP requests from
         * being displayed in the log. Logs containing tokens, passwords or custom function
         * arguments and the result of computing these will be obfuscated by default. Logs will not
         * be obfuscated if the value is set to `null`.
         *
         * @param httpLogObfuscator the HTTP log obfuscator to be used or `null` if obfuscation
         * should be disabled.
         * @return the Builder instance used.
         */
        public fun httpLogObfuscator(httpLogObfuscator: HttpLogObfuscator?): Builder = apply {
            this.httpLogObfuscator = httpLogObfuscator
        }

        /**
         * Sets the name of the HTTP header used to send authorization data in when making requests to
         * Atlas App Services. The Atlas App or firewall must have been configured to expect a
         * custom authorization header.
         *
         * The default authorization header is named [DEFAULT_AUTHORIZATION_HEADER_NAME].
         *
         * @param name name of the header.
         * @throws IllegalArgumentException if an empty [name] is provided.
         */
        public fun authorizationHeaderName(name: String): Builder = apply {
            require(name.isNotEmpty()) { "Non-empty 'name' required." }
            authorizationHeaderName = name
        }

        /**
         * Update the custom headers that would be appended to every request to an Atlas App Services Application.
         *
         * @param block lambda with the the custom header map update instructions.
         * @throws IllegalArgumentException if an empty header name is provided.
         */
        public fun customRequestHeaders(
            block: MutableMap.() -> Unit,
        ): Builder = apply {
            customRequestHeaders.block()
            require(!customRequestHeaders.containsKey("")) { "Non-empty custom header name required." }
        }

        /**
         * Sets the default EJson decoder that would be use to encode and decode arguments and results
         * when calling remote Atlas [Functions], authenticating with a [customFunction], and retrieving
         * a user [profile] or [customData].
         */
        @ExperimentalRealmSerializerApi
        @OptIn(ExperimentalKBsonSerializerApi::class)
        public fun ejson(ejson: EJson): Builder = apply {
            this.ejson = ejson
        }

        /**
         * If enabled, a single connection is used for all Realms opened
         * with a single sync user. If disabled, a separate connection is used for each
         * Realm.
         *
         * Session multiplexing reduces resources used and typically improves
         * performance. When multiplexing is enabled, the connection is not immediately
         * closed when the last session is closed, but remains open for
         * [SyncTimeoutOptions.connectionLingerTime] as defined by [syncTimeouts] (30 seconds by
         * default).
         */
        public fun enableSessionMultiplexing(enabled: Boolean): Builder {
            this.enableSessionMultiplexing = enabled
            return this
        }

        /**
         *  Configure the assorted types of connection timeouts for sync connections.
         *  See [SyncTimeoutOptionsBuilder] for a description of each option.
         */
        public fun syncTimeouts(action: SyncTimeoutOptionsBuilder.() -> Unit): Builder {
            val builder = SyncTimeoutOptionsBuilder()
            action(builder)
            syncTimeoutOptions = builder.build()
            return this
        }

        /**
         * Platform Networking offer improved support for proxies and firewalls that require authentication,
         * instead of Realm's built-in WebSocket client for Sync traffic. This will become the default in a future version.
         *
         * Note: Only Android and JVM targets are supported so far.
         */
        public fun usePlatformNetworking(enable: Boolean = true): Builder =
            apply {
                this.usePlatformNetworking = enable
            }

        /**
         * Allows defining a custom network transport. It is used by some tests that require simulating
         * network responses.
         */
        internal fun networkTransport(networkTransport: NetworkTransport?): Builder = apply {
            this.networkTransport = networkTransport
        }

        /**
         * Creates the AppConfiguration from the properties of the builder.
         *
         * @return the AppConfiguration that can be used to create a [App].
         */
        public fun build(): AppConfiguration {
            // We cannot rewire this to build(bundleId) and just have REPLACED_BY_IR here,
            // as these calls might be in a module where the compiler plugin hasn't been applied.
            // In that case we don't setup the correct bundle ID. If this is an issue we could maybe
            // just force users to apply our plugin.
            return build("UNKNOWN_BUNDLE_ID")
        }

        // This method is used to inject bundleId to the sync configuration. The
        // SyncLoweringExtension is replacing calls to SyncConfiguration.Builder.build() with calls
        // to this method.
        @OptIn(ExperimentalKBsonSerializerApi::class)
        public fun build(bundleId: String): AppConfiguration {
            // Configure logging during creation of AppConfiguration to keep old behavior for
            // configuring logging. This should be removed when `LogConfiguration` is removed.
            val allLoggers = mutableListOf()
            allLoggers.addAll(userLoggers)

            val logConfig = this.logLevel?.let {
                RealmLog.level = it
                LogConfiguration(it, allLoggers)
            }

            userLoggers.forEach { RealmLog.add(it) }

            val appNetworkDispatcherFactory = if (dispatcher != null) {
                CoroutineDispatcherFactory.unmanaged(dispatcher!!)
            } else {
                // TODO We should consider using a multi threaded dispatcher. Ktor already does
                //  this under the hood though, so it is unclear exactly what benefit there is.
                //  https://github.com/realm/realm-kotlin/issues/501
                CoroutineDispatcherFactory.managed("app-dispatcher-$appId")
            }

            val appLogger = ContextLogger("Sdk")
            val networkTransport: (dispatcher: DispatcherHolder) -> NetworkTransport =
                { dispatcherHolder ->
                    val logger: Logger = object : Logger {
                        override fun log(message: String) {
                            val obfuscatedMessage = httpLogObfuscator?.obfuscate(message)
                            appLogger.debug(obfuscatedMessage ?: message)
                        }
                    }
                    networkTransport ?: KtorNetworkTransport(
                        // FIXME Add AppConfiguration.Builder option to set timeout as a Duration with default \
                        //  constant in AppConfiguration.Companion
                        //  https://github.com/realm/realm-kotlin/issues/408
                        timeoutMs = 120_000,
                        dispatcherHolder = dispatcherHolder,
                        logger = logger,
                        customHeaders = customRequestHeaders,
                        authorizationHeaderName = authorizationHeaderName
                    )
                }

            val websocketTransport: WebSocketTransport? =
                if (usePlatformNetworking) {
                    websocketTransport ?: RealmWebSocketTransport(
                        timeoutMs = 60000
                    )
                } else null

            return AppConfigurationImpl(
                appId = appId,
                baseUrl = baseUrl,
                encryptionKey = encryptionKey,
                metadataMode = if (encryptionKey == null)
                    MetadataMode.RLM_SYNC_CLIENT_METADATA_MODE_PLAINTEXT
                else MetadataMode.RLM_SYNC_CLIENT_METADATA_MODE_ENCRYPTED,
                appNetworkDispatcherFactory = appNetworkDispatcherFactory,
                networkTransportFactory = networkTransport,
                websocketTransport = websocketTransport,
                syncRootDirectory = syncRootDirectory,
                logger = logConfig,
                appName = appName,
                appVersion = appVersion,
                bundleId = bundleId,
                ejson = ejson,
                httpLogObfuscator = httpLogObfuscator,
                customRequestHeaders = customRequestHeaders,
                authorizationHeaderName = authorizationHeaderName,
                enableSessionMultiplexing = enableSessionMultiplexing,
                syncTimeoutOptions = syncTimeoutOptions,
            )
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy