diff --git a/android/src/main/java/io/ably/lib/push/ActivationContext.java b/android/src/main/java/io/ably/lib/push/ActivationContext.java index addb7d4eb..849631248 100644 --- a/android/src/main/java/io/ably/lib/push/ActivationContext.java +++ b/android/src/main/java/io/ably/lib/push/ActivationContext.java @@ -60,6 +60,7 @@ public void setAbly(AblyRest ably) { this.clientId = ably.auth.clientId; } + @SuppressWarnings("deprecation") // internal push-registration client, not an application entry point AblyRest getAbly() throws AblyException { if(ably != null) { Log.v(TAG, "getAbly(): returning existing Ably instance"); @@ -84,6 +85,7 @@ AblyRest getAbly() throws AblyException { * @return AblyRest instance with device identity token auth. We use this instance to perform * deregistration calls in push activation flow. */ + @SuppressWarnings("deprecation") // internal push-registration client, not an application entry point AblyRest getDeviceIdentityTokenBasedAblyClient(String deviceIdentityToken) throws AblyException { ClientOptions clientOptions = ably.options.copy(); clientOptions.clearAuthOptions(); diff --git a/android/src/main/java/io/ably/lib/rest/AblyRest.java b/android/src/main/java/io/ably/lib/rest/AblyRest.java index 7f04feb2a..fd853ced0 100644 --- a/android/src/main/java/io/ably/lib/rest/AblyRest.java +++ b/android/src/main/java/io/ably/lib/rest/AblyRest.java @@ -20,7 +20,11 @@ public class AblyRest extends AblyBase { * Spec: RSC1 * @param key The Ably API key or token string used to validate the client. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(String key) throws AblyException { super(key, new AndroidPlatformAgentProvider()); } @@ -31,7 +35,11 @@ public AblyRest(String key) throws AblyException { * Spec: RSC1 * @param options A {@link ClientOptions} object to configure the client connection to Ably. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(ClientOptions options) throws AblyException { super(options, new AndroidPlatformAgentProvider()); } diff --git a/build.gradle.kts b/build.gradle.kts index a98b165b6..308f58e4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.test.retry) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.compose) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e935db93..8664438d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,4 +91,5 @@ lombok = { id = "io.freefair.lombok", version.ref = "lombok" } test-retry = { id = "org.gradle.test-retry", version.ref = "test-retry" } android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/java/src/main/java/io/ably/lib/rest/AblyRest.java b/java/src/main/java/io/ably/lib/rest/AblyRest.java index 7ab6a3390..682c019ff 100644 --- a/java/src/main/java/io/ably/lib/rest/AblyRest.java +++ b/java/src/main/java/io/ably/lib/rest/AblyRest.java @@ -17,7 +17,11 @@ public class AblyRest extends AblyBase { * Spec: RSC1 * @param key The Ably API key or token string used to validate the client. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.server.PubSubServer#httpClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(String key) throws AblyException { super(key, new JavaPlatformAgentProvider()); } @@ -28,7 +32,11 @@ public AblyRest(String key) throws AblyException { * Spec: RSC1 * @param options A {@link ClientOptions} object to configure the client connection to Ably. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.server.PubSubServer#httpClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact instead, which names the side of the + * connection your code runs on. */ + @Deprecated public AblyRest(ClientOptions options) throws AblyException { super(options, new JavaPlatformAgentProvider()); } diff --git a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java index b991ed63b..efd8bcad6 100644 --- a/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java +++ b/lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java @@ -56,7 +56,12 @@ public class AblyRealtime extends AblyRest { * Spec: RSC1 * @param key The Ably API key or token string used to validate the client. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact if this code runs on an end-user device, or + * {@code io.ably.pubsub.server.PubSubServer#realtimeClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact if it runs on infrastructure you control. */ + @Deprecated public AblyRealtime(String key) throws AblyException { this(new ClientOptions(key)); } @@ -67,7 +72,12 @@ public AblyRealtime(String key) throws AblyException { * Spec: RSC1 * @param options A {@link ClientOptions} object. * @throws AblyException + * @deprecated use {@code io.ably.pubsub.device.PubSubDevice#clientBuilder()} from the + * {@code io.ably.pubsub:device} artifact if this code runs on an end-user device, or + * {@code io.ably.pubsub.server.PubSubServer#realtimeClientBuilder()} from the + * {@code io.ably.pubsub:server} artifact if it runs on infrastructure you control. */ + @Deprecated public AblyRealtime(ClientOptions options) throws AblyException { super(options); final InternalChannels channels = new InternalChannels(); diff --git a/pubsub-device/build.gradle.kts b/pubsub-device/build.gradle.kts new file mode 100644 index 000000000..9d6753740 --- /dev/null +++ b/pubsub-device/build.gradle.kts @@ -0,0 +1,89 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinMultiplatform +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.maven.publish) +} + +kotlin { + explicitApi() + + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + publishLibraryVariants("release") + } + + jvm { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + } + + sourceSets { + commonMain.dependencies { + /* + * The two platform artifacts publish the same io.ably.lib.* types, so common code + * compiles against either one. ably-java is the arbitrary pick; each target below + * brings the real one. + */ + compileOnly(project(":java")) + } + androidMain.dependencies { + api(project(":android")) + } + jvmMain.dependencies { + api(project(":java")) + } + commonTest.dependencies { + compileOnly(project(":java")) + implementation(kotlin("test")) + } + } +} + +android { + namespace = "io.ably.pubsub.device" + compileSdk = 34 + + defaultConfig { + minSdk = 19 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + + lint { + abortOnError = false + } + + testOptions { + targetSdk = 34 + /* + * AndroidPlatformAgentProvider reads android.os.Build.VERSION.SDK_INT, which is an unmocked + * stub in local unit tests. Without this it throws instead of returning a default. + */ + unitTests.isReturnDefaultValues = true + } +} + +mavenPublishing { + configure(KotlinMultiplatform(javadocJar = JavadocJar.Empty(), androidVariantsToPublish = listOf("release"))) +} + +/* check.yml invokes `runUnitTests` unqualified across all projects. */ +tasks.register("runUnitTests") { + dependsOn("jvmTest", "testDebugUnitTest") +} diff --git a/pubsub-device/gradle.properties b/pubsub-device/gradle.properties new file mode 100644 index 000000000..e251157c9 --- /dev/null +++ b/pubsub-device/gradle.properties @@ -0,0 +1,4 @@ +GROUP=io.ably.pubsub +POM_ARTIFACT_ID=device +POM_NAME=Ably Pub/Sub SDK for devices +POM_DESCRIPTION=Ably Pub/Sub SDK for applications running on end-user devices, for Android and the JVM. diff --git a/pubsub-device/src/androidMain/AndroidManifest.xml b/pubsub-device/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..a2f47b605 --- /dev/null +++ b/pubsub-device/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt b/pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt new file mode 100644 index 000000000..31f40256f --- /dev/null +++ b/pubsub-device/src/commonMain/kotlin/io/ably/pubsub/device/PubSubDevice.kt @@ -0,0 +1,423 @@ +package io.ably.pubsub.device + +import io.ably.lib.push.Storage +import io.ably.lib.realtime.AblyRealtime +import io.ably.lib.rest.Auth +import io.ably.lib.types.ClientOptions +import io.ably.lib.types.Param +import io.ably.lib.types.ProxyOptions +import io.ably.lib.util.Log + +/** + * Entry point for the Ably Pub/Sub SDK for devices: applications running on end-user devices, whose + * connections are counted on accounts with monthly-active-user billing. + * + * Clients built here are the same [AblyRealtime] objects the core SDK has always returned, and + * behave identically. What the artifact adds is the choice itself: the dependency you declare and + * the factory you call state which side of the connection your code runs on, rather than leaving it + * to be inferred. + * + * If your code runs on infrastructure you control, use the `io.ably.pubsub:server` artifact instead. + * + * There is a single door because there is a single choice to make. Connectionless operations — + * history, presence queries, token requests, `request()` and `batchPublish()` — are all available on + * the client this returns. + * + * ``` + * val client = PubSubDevice.clientBuilder() + * .key("xVLyHw.MHOCLg:...") + * .clientId("bob") + * .build() + * ``` + */ +public object PubSubDevice { + + /** + * Creates a builder for a device client. It exposes one method per [ClientOptions] property. + * + * @return a new builder. + */ + @JvmStatic + public fun clientBuilder(): ClientBuilder = ClientBuilder() + + /** + * Builds a device client. Obtain one from [PubSubDevice.clientBuilder]. + */ + public class ClientBuilder internal constructor() { + + /** Accumulates the calls made on this builder; handed to the client as-is by [build]. */ + private val options = ClientOptions() + + /** + * Sets [Auth.AuthOptions.authCallback]. + * + * @param authCallback the value to set. + * @return this builder. + */ + public fun authCallback(authCallback: Auth.TokenCallback): ClientBuilder = apply { options.authCallback = authCallback } + + /** + * Sets [Auth.AuthOptions.authUrl]. + * + * @param authUrl the value to set. + * @return this builder. + */ + public fun authUrl(authUrl: String): ClientBuilder = apply { options.authUrl = authUrl } + + /** + * Sets [Auth.AuthOptions.authMethod]. + * + * @param authMethod the value to set. + * @return this builder. + */ + public fun authMethod(authMethod: String): ClientBuilder = apply { options.authMethod = authMethod } + + /** + * Sets [Auth.AuthOptions.key]. + * + * @param key the value to set. + * @return this builder. + */ + public fun key(key: String): ClientBuilder = apply { options.key = key } + + /** + * Sets [Auth.AuthOptions.token]. + * + * @param token the value to set. + * @return this builder. + */ + public fun token(token: String): ClientBuilder = apply { options.token = token } + + /** + * Sets [Auth.AuthOptions.tokenDetails]. + * + * @param tokenDetails the value to set. + * @return this builder. + */ + public fun tokenDetails(tokenDetails: Auth.TokenDetails): ClientBuilder = apply { options.tokenDetails = tokenDetails } + + /** + * Sets [Auth.AuthOptions.authHeaders]. + * + * @param authHeaders the value to set. + * @return this builder. + */ + public fun authHeaders(authHeaders: Array): ClientBuilder = apply { options.authHeaders = authHeaders } + + /** + * Sets [Auth.AuthOptions.authParams]. + * + * @param authParams the value to set. + * @return this builder. + */ + public fun authParams(authParams: Array): ClientBuilder = apply { options.authParams = authParams } + + /** + * Sets [Auth.AuthOptions.queryTime]. + * + * @param queryTime the value to set. + * @return this builder. + */ + public fun queryTime(queryTime: Boolean): ClientBuilder = apply { options.queryTime = queryTime } + + /** + * Sets [Auth.AuthOptions.useTokenAuth]. + * + * @param useTokenAuth the value to set. + * @return this builder. + */ + public fun useTokenAuth(useTokenAuth: Boolean): ClientBuilder = apply { options.useTokenAuth = useTokenAuth } + + /** + * Sets [ClientOptions.clientId]. + * + * @param clientId the value to set. + * @return this builder. + */ + public fun clientId(clientId: String): ClientBuilder = apply { options.clientId = clientId } + + /** + * Sets [ClientOptions.logLevel]. + * + * @param logLevel the value to set. + * @return this builder. + */ + public fun logLevel(logLevel: Int): ClientBuilder = apply { options.logLevel = logLevel } + + /** + * Sets [ClientOptions.logHandler]. + * + * @param logHandler the value to set. + * @return this builder. + */ + public fun logHandler(logHandler: Log.LogHandler): ClientBuilder = apply { options.logHandler = logHandler } + + /** + * Sets [ClientOptions.tls]. + * + * @param tls the value to set. + * @return this builder. + */ + public fun tls(tls: Boolean): ClientBuilder = apply { options.tls = tls } + + /** + * Sets [ClientOptions.headers]. + * + * @param headers the value to set. + * @return this builder. + */ + public fun headers(headers: Map): ClientBuilder = apply { options.headers = headers } + + /** + * Sets [ClientOptions.restHost]. + * + * @param restHost the value to set. + * @return this builder. + */ + public fun restHost(restHost: String): ClientBuilder = apply { options.restHost = restHost } + + /** + * Sets [ClientOptions.port]. + * + * @param port the value to set. + * @return this builder. + */ + public fun port(port: Int): ClientBuilder = apply { options.port = port } + + /** + * Sets [ClientOptions.tlsPort]. + * + * @param tlsPort the value to set. + * @return this builder. + */ + public fun tlsPort(tlsPort: Int): ClientBuilder = apply { options.tlsPort = tlsPort } + + /** + * Sets [ClientOptions.useBinaryProtocol]. + * + * @param useBinaryProtocol the value to set. + * @return this builder. + */ + public fun useBinaryProtocol(useBinaryProtocol: Boolean): ClientBuilder = apply { options.useBinaryProtocol = useBinaryProtocol } + + /** + * Sets [ClientOptions.proxy]. + * + * @param proxy the value to set. + * @return this builder. + */ + public fun proxy(proxy: ProxyOptions): ClientBuilder = apply { options.proxy = proxy } + + /** + * Sets [ClientOptions.environment]. + * + * @param environment the value to set. + * @return this builder. + */ + public fun environment(environment: String): ClientBuilder = apply { options.environment = environment } + + /** + * Sets [ClientOptions.idempotentRestPublishing]. + * + * @param idempotentRestPublishing the value to set. + * @return this builder. + */ + public fun idempotentRestPublishing(idempotentRestPublishing: Boolean): ClientBuilder = apply { options.idempotentRestPublishing = idempotentRestPublishing } + + /** + * Sets [ClientOptions.httpOpenTimeout]. + * + * @param httpOpenTimeout the value to set. + * @return this builder. + */ + public fun httpOpenTimeout(httpOpenTimeout: Int): ClientBuilder = apply { options.httpOpenTimeout = httpOpenTimeout } + + /** + * Sets [ClientOptions.httpRequestTimeout]. + * + * @param httpRequestTimeout the value to set. + * @return this builder. + */ + public fun httpRequestTimeout(httpRequestTimeout: Int): ClientBuilder = apply { options.httpRequestTimeout = httpRequestTimeout } + + /** + * Sets [ClientOptions.httpMaxRetryDuration]. + * + * @param httpMaxRetryDuration the value to set. + * @return this builder. + */ + public fun httpMaxRetryDuration(httpMaxRetryDuration: Int): ClientBuilder = apply { options.httpMaxRetryDuration = httpMaxRetryDuration } + + /** + * Sets [ClientOptions.httpMaxRetryCount]. + * + * @param httpMaxRetryCount the value to set. + * @return this builder. + */ + public fun httpMaxRetryCount(httpMaxRetryCount: Int): ClientBuilder = apply { options.httpMaxRetryCount = httpMaxRetryCount } + + /** + * Sets [ClientOptions.fallbackHosts]. + * + * @param fallbackHosts the value to set. + * @return this builder. + */ + public fun fallbackHosts(fallbackHosts: Array): ClientBuilder = apply { options.fallbackHosts = fallbackHosts } + + /** + * Sets [ClientOptions.fallbackHostsUseDefault]. + * + * @param fallbackHostsUseDefault the value to set. + * @return this builder. + */ + @Deprecated("Deprecated on ClientOptions itself; use fallbackHosts to supply custom hosts.") + @Suppress("DEPRECATION") + public fun fallbackHostsUseDefault(fallbackHostsUseDefault: Boolean): ClientBuilder = apply { options.fallbackHostsUseDefault = fallbackHostsUseDefault } + + /** + * Sets [ClientOptions.fallbackRetryTimeout]. + * + * @param fallbackRetryTimeout the value to set. + * @return this builder. + */ + public fun fallbackRetryTimeout(fallbackRetryTimeout: Long): ClientBuilder = apply { options.fallbackRetryTimeout = fallbackRetryTimeout } + + /** + * Sets [ClientOptions.defaultTokenParams]. + * + * @param defaultTokenParams the value to set. + * @return this builder. + */ + public fun defaultTokenParams(defaultTokenParams: Auth.TokenParams): ClientBuilder = apply { options.defaultTokenParams = defaultTokenParams } + + /** + * Sets [ClientOptions.asyncHttpThreadpoolSize]. + * + * @param asyncHttpThreadpoolSize the value to set. + * @return this builder. + */ + public fun asyncHttpThreadpoolSize(asyncHttpThreadpoolSize: Int): ClientBuilder = apply { options.asyncHttpThreadpoolSize = asyncHttpThreadpoolSize } + + /** + * Sets [ClientOptions.pushFullWait]. + * + * @param pushFullWait the value to set. + * @return this builder. + */ + public fun pushFullWait(pushFullWait: Boolean): ClientBuilder = apply { options.pushFullWait = pushFullWait } + + /** + * Sets [ClientOptions.localStorage]. + * + * @param localStorage the value to set. + * @return this builder. + */ + public fun localStorage(localStorage: Storage): ClientBuilder = apply { options.localStorage = localStorage } + + /** + * Sets [ClientOptions.addRequestIds]. + * + * @param addRequestIds the value to set. + * @return this builder. + */ + public fun addRequestIds(addRequestIds: Boolean): ClientBuilder = apply { options.addRequestIds = addRequestIds } + + /** + * Sets [ClientOptions.agents]. + * + * @param agents the value to set. + * @return this builder. + */ + public fun agents(agents: Map): ClientBuilder = apply { options.agents = agents } + + /** + * Sets [ClientOptions.realtimeHost]. + * + * @param realtimeHost the value to set. + * @return this builder. + */ + public fun realtimeHost(realtimeHost: String): ClientBuilder = apply { options.realtimeHost = realtimeHost } + + /** + * Sets [ClientOptions.autoConnect]. + * + * @param autoConnect the value to set. + * @return this builder. + */ + public fun autoConnect(autoConnect: Boolean): ClientBuilder = apply { options.autoConnect = autoConnect } + + /** + * Sets [ClientOptions.queueMessages]. + * + * @param queueMessages the value to set. + * @return this builder. + */ + public fun queueMessages(queueMessages: Boolean): ClientBuilder = apply { options.queueMessages = queueMessages } + + /** + * Sets [ClientOptions.echoMessages]. + * + * @param echoMessages the value to set. + * @return this builder. + */ + public fun echoMessages(echoMessages: Boolean): ClientBuilder = apply { options.echoMessages = echoMessages } + + /** + * Sets [ClientOptions.recover]. + * + * @param recover the value to set. + * @return this builder. + */ + public fun recover(recover: String): ClientBuilder = apply { options.recover = recover } + + /** + * Sets [ClientOptions.realtimeRequestTimeout]. + * + * @param realtimeRequestTimeout the value to set. + * @return this builder. + */ + public fun realtimeRequestTimeout(realtimeRequestTimeout: Long): ClientBuilder = apply { options.realtimeRequestTimeout = realtimeRequestTimeout } + + /** + * Sets [ClientOptions.disconnectedRetryTimeout]. + * + * @param disconnectedRetryTimeout the value to set. + * @return this builder. + */ + public fun disconnectedRetryTimeout(disconnectedRetryTimeout: Long): ClientBuilder = apply { options.disconnectedRetryTimeout = disconnectedRetryTimeout } + + /** + * Sets [ClientOptions.suspendedRetryTimeout]. + * + * @param suspendedRetryTimeout the value to set. + * @return this builder. + */ + public fun suspendedRetryTimeout(suspendedRetryTimeout: Long): ClientBuilder = apply { options.suspendedRetryTimeout = suspendedRetryTimeout } + + /** + * Sets [ClientOptions.channelRetryTimeout]. + * + * @param channelRetryTimeout the value to set. + * @return this builder. + */ + public fun channelRetryTimeout(channelRetryTimeout: Int): ClientBuilder = apply { options.channelRetryTimeout = channelRetryTimeout } + + /** + * Sets [ClientOptions.transportParams]. + * + * @param transportParams the value to set. + * @return this builder. + */ + public fun transportParams(transportParams: Array): ClientBuilder = apply { options.transportParams = transportParams } + + /** + * Builds the client, which connects immediately unless [autoConnect] was set to false. + * + * @return an [AblyRealtime]. + * @throws io.ably.lib.types.AblyException if the options are invalid, for example if no + * authentication parameters were supplied. + */ + @Suppress("DEPRECATION") // this factory is the replacement for that constructor + public fun build(): AblyRealtime = AblyRealtime(options) + } +} diff --git a/pubsub-server/build.gradle.kts b/pubsub-server/build.gradle.kts new file mode 100644 index 000000000..04b83afdf --- /dev/null +++ b/pubsub-server/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(libs.plugins.maven.publish) + checkstyle + `java-library` +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + /* + * api, not implementation: the factories return io.ably.lib.rest.AblyRest and + * io.ably.lib.realtime.AblyRealtime, and consumers configure them with io.ably.lib.types.*, + * so the core is part of this module's compile-time ABI. + */ + api(project(":java")) + testImplementation(libs.bundles.tests) +} + +tasks.register("runUnitTests") { + beforeTest(closureOf { logger.lifecycle("-> $this") }) + outputs.upToDateWhen { false } +} diff --git a/pubsub-server/gradle.properties b/pubsub-server/gradle.properties new file mode 100644 index 000000000..5abdf4810 --- /dev/null +++ b/pubsub-server/gradle.properties @@ -0,0 +1,5 @@ +GROUP=io.ably.pubsub +POM_ARTIFACT_ID=server +POM_NAME=Ably Pub/Sub SDK for servers +POM_DESCRIPTION=Ably Pub/Sub SDK for server-side JVM applications. +POM_PACKAGING=jar diff --git a/pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java b/pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java new file mode 100644 index 000000000..ce709a5f5 --- /dev/null +++ b/pubsub-server/src/main/java/io/ably/pubsub/server/PubSubServer.java @@ -0,0 +1,632 @@ +package io.ably.pubsub.server; + +import io.ably.lib.push.Storage; +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.rest.AblyRest; +import io.ably.lib.rest.Auth; +import io.ably.lib.types.AblyException; +import io.ably.lib.types.ClientOptions; +import io.ably.lib.types.Param; +import io.ably.lib.types.ProxyOptions; +import io.ably.lib.util.Log.LogHandler; + +import java.util.Map; + +/** + * Entry point for the Ably Pub/Sub SDK for servers: applications running on infrastructure you + * control, whose traffic is exempt from monthly-active-user billing. + *

+ * Clients built here are the same {@link AblyRest} and {@link AblyRealtime} objects the core SDK + * has always returned, and behave identically. What the artifact adds is the choice itself: the + * dependency you declare and the factory you call state which side of the connection your code runs + * on, rather than leaving it to be inferred. + *

+ * If your code runs on an end-user device, use the {@code io.ably.pubsub:device} artifact instead. + * + *

{@code
+ * AblyRest http = PubSubServer.httpClientBuilder()
+ *     .key("xVLyHw.MHOCLg:...")
+ *     .build();
+ *
+ * AblyRealtime realtime = PubSubServer.realtimeClientBuilder()
+ *     .key("xVLyHw.MHOCLg:...")
+ *     .echoMessages(false)
+ *     .build();
+ * }
+ */ +public final class PubSubServer { + + private PubSubServer() { + } + + /** + * Creates a builder for a stateless HTTP client, which talks to Ably over plain HTTP requests + * without holding a connection open. + *

+ * This is the right choice for most server-side work: publishing, reading history, querying + * presence, issuing tokens and push administration. + * + * @return a new builder. + */ + public static HttpClientBuilder httpClientBuilder() { + return new HttpClientBuilder(); + } + + /** + * Creates a builder for a realtime client, which holds a persistent connection to Ably and can + * subscribe to messages and presence as they happen. + *

+ * Choose this over {@link #httpClientBuilder()} only when the server needs to receive messages + * live, rather than only send them. + * + * @return a new builder. + */ + public static RealtimeClientBuilder realtimeClientBuilder() { + return new RealtimeClientBuilder(); + } + + /** + * The options common to both server-side clients. One method per {@link ClientOptions} + * property that can affect a client of either kind; {@link RealtimeClientBuilder} adds the + * properties that only mean something for a persistent connection. + * + * @param the concrete builder type, so that chaining preserves it. + */ + public abstract static class ClientBuilder> { + + /** Accumulates the calls made on this builder; handed to the client as-is by build(). */ + final ClientOptions options = new ClientOptions(); + + ClientBuilder() { + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + /** + * Sets {@link Auth.AuthOptions#authCallback}. + * + * @param authCallback the value to set. + * @return this builder. + */ + public T authCallback(Auth.TokenCallback authCallback) { + options.authCallback = authCallback; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authUrl}. + * + * @param authUrl the value to set. + * @return this builder. + */ + public T authUrl(String authUrl) { + options.authUrl = authUrl; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authMethod}. + * + * @param authMethod the value to set. + * @return this builder. + */ + public T authMethod(String authMethod) { + options.authMethod = authMethod; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#key}. + * + * @param key the value to set. + * @return this builder. + */ + public T key(String key) { + options.key = key; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#token}. + * + * @param token the value to set. + * @return this builder. + */ + public T token(String token) { + options.token = token; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#tokenDetails}. + * + * @param tokenDetails the value to set. + * @return this builder. + */ + public T tokenDetails(Auth.TokenDetails tokenDetails) { + options.tokenDetails = tokenDetails; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authHeaders}. + * + * @param authHeaders the value to set. + * @return this builder. + */ + public T authHeaders(Param[] authHeaders) { + options.authHeaders = authHeaders; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#authParams}. + * + * @param authParams the value to set. + * @return this builder. + */ + public T authParams(Param[] authParams) { + options.authParams = authParams; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#queryTime}. + * + * @param queryTime the value to set. + * @return this builder. + */ + public T queryTime(boolean queryTime) { + options.queryTime = queryTime; + return self(); + } + + /** + * Sets {@link Auth.AuthOptions#useTokenAuth}. + * + * @param useTokenAuth the value to set. + * @return this builder. + */ + public T useTokenAuth(boolean useTokenAuth) { + options.useTokenAuth = useTokenAuth; + return self(); + } + + /** + * Sets {@link ClientOptions#clientId}. + * + * @param clientId the value to set. + * @return this builder. + */ + public T clientId(String clientId) { + options.clientId = clientId; + return self(); + } + + /** + * Sets {@link ClientOptions#logLevel}. + * + * @param logLevel the value to set. + * @return this builder. + */ + public T logLevel(int logLevel) { + options.logLevel = logLevel; + return self(); + } + + /** + * Sets {@link ClientOptions#logHandler}. + * + * @param logHandler the value to set. + * @return this builder. + */ + public T logHandler(LogHandler logHandler) { + options.logHandler = logHandler; + return self(); + } + + /** + * Sets {@link ClientOptions#tls}. + * + * @param tls the value to set. + * @return this builder. + */ + public T tls(boolean tls) { + options.tls = tls; + return self(); + } + + /** + * Sets {@link ClientOptions#headers}. + * + * @param headers the value to set. + * @return this builder. + */ + public T headers(Map headers) { + options.headers = headers; + return self(); + } + + /** + * Sets {@link ClientOptions#restHost}. + * + * @param restHost the value to set. + * @return this builder. + */ + public T restHost(String restHost) { + options.restHost = restHost; + return self(); + } + + /** + * Sets {@link ClientOptions#port}. + * + * @param port the value to set. + * @return this builder. + */ + public T port(int port) { + options.port = port; + return self(); + } + + /** + * Sets {@link ClientOptions#tlsPort}. + * + * @param tlsPort the value to set. + * @return this builder. + */ + public T tlsPort(int tlsPort) { + options.tlsPort = tlsPort; + return self(); + } + + /** + * Sets {@link ClientOptions#useBinaryProtocol}. + * + * @param useBinaryProtocol the value to set. + * @return this builder. + */ + public T useBinaryProtocol(boolean useBinaryProtocol) { + options.useBinaryProtocol = useBinaryProtocol; + return self(); + } + + /** + * Sets {@link ClientOptions#proxy}. + * + * @param proxy the value to set. + * @return this builder. + */ + public T proxy(ProxyOptions proxy) { + options.proxy = proxy; + return self(); + } + + /** + * Sets {@link ClientOptions#environment}. + * + * @param environment the value to set. + * @return this builder. + */ + public T environment(String environment) { + options.environment = environment; + return self(); + } + + /** + * Sets {@link ClientOptions#idempotentRestPublishing}. + * + * @param idempotentRestPublishing the value to set. + * @return this builder. + */ + public T idempotentRestPublishing(boolean idempotentRestPublishing) { + options.idempotentRestPublishing = idempotentRestPublishing; + return self(); + } + + /** + * Sets {@link ClientOptions#httpOpenTimeout}. + * + * @param httpOpenTimeout the value to set. + * @return this builder. + */ + public T httpOpenTimeout(int httpOpenTimeout) { + options.httpOpenTimeout = httpOpenTimeout; + return self(); + } + + /** + * Sets {@link ClientOptions#httpRequestTimeout}. + * + * @param httpRequestTimeout the value to set. + * @return this builder. + */ + public T httpRequestTimeout(int httpRequestTimeout) { + options.httpRequestTimeout = httpRequestTimeout; + return self(); + } + + /** + * Sets {@link ClientOptions#httpMaxRetryDuration}. + * + * @param httpMaxRetryDuration the value to set. + * @return this builder. + */ + public T httpMaxRetryDuration(int httpMaxRetryDuration) { + options.httpMaxRetryDuration = httpMaxRetryDuration; + return self(); + } + + /** + * Sets {@link ClientOptions#httpMaxRetryCount}. + * + * @param httpMaxRetryCount the value to set. + * @return this builder. + */ + public T httpMaxRetryCount(int httpMaxRetryCount) { + options.httpMaxRetryCount = httpMaxRetryCount; + return self(); + } + + /** + * Sets {@link ClientOptions#fallbackHosts}. + * + * @param fallbackHosts the value to set. + * @return this builder. + */ + public T fallbackHosts(String[] fallbackHosts) { + options.fallbackHosts = fallbackHosts; + return self(); + } + + /** + * Sets {@link ClientOptions#fallbackHostsUseDefault}. + * + * @param fallbackHostsUseDefault the value to set. + * @return this builder. + * @deprecated deprecated on {@link ClientOptions} itself; use + * {@link #fallbackHosts(String[])} to supply custom hosts. + */ + @Deprecated + public T fallbackHostsUseDefault(boolean fallbackHostsUseDefault) { + options.fallbackHostsUseDefault = fallbackHostsUseDefault; + return self(); + } + + /** + * Sets {@link ClientOptions#fallbackRetryTimeout}. + * + * @param fallbackRetryTimeout the value to set. + * @return this builder. + */ + public T fallbackRetryTimeout(long fallbackRetryTimeout) { + options.fallbackRetryTimeout = fallbackRetryTimeout; + return self(); + } + + /** + * Sets {@link ClientOptions#defaultTokenParams}. + * + * @param defaultTokenParams the value to set. + * @return this builder. + */ + public T defaultTokenParams(Auth.TokenParams defaultTokenParams) { + options.defaultTokenParams = defaultTokenParams; + return self(); + } + + /** + * Sets {@link ClientOptions#asyncHttpThreadpoolSize}. + * + * @param asyncHttpThreadpoolSize the value to set. + * @return this builder. + */ + public T asyncHttpThreadpoolSize(int asyncHttpThreadpoolSize) { + options.asyncHttpThreadpoolSize = asyncHttpThreadpoolSize; + return self(); + } + + /** + * Sets {@link ClientOptions#pushFullWait}. + * + * @param pushFullWait the value to set. + * @return this builder. + */ + public T pushFullWait(boolean pushFullWait) { + options.pushFullWait = pushFullWait; + return self(); + } + + /** + * Sets {@link ClientOptions#localStorage}. + * + * @param localStorage the value to set. + * @return this builder. + */ + public T localStorage(Storage localStorage) { + options.localStorage = localStorage; + return self(); + } + + /** + * Sets {@link ClientOptions#addRequestIds}. + * + * @param addRequestIds the value to set. + * @return this builder. + */ + public T addRequestIds(boolean addRequestIds) { + options.addRequestIds = addRequestIds; + return self(); + } + + /** + * Sets {@link ClientOptions#agents}. + * + * @param agents the value to set. + * @return this builder. + */ + public T agents(Map agents) { + options.agents = agents; + return self(); + } + } + + /** + * Builds a stateless HTTP client. Obtain one from {@link PubSubServer#httpClientBuilder()}. + *

+ * Deliberately does not expose the realtime-only options, since an {@link AblyRest} never + * opens a connection for them to apply to. + */ + public static final class HttpClientBuilder extends ClientBuilder { + + HttpClientBuilder() { + } + + /** + * Builds the client. + * + * @return an {@link AblyRest}. + * @throws AblyException if the options are invalid, for example if no authentication + * parameters were supplied. + */ + @SuppressWarnings("deprecation") // this factory is the replacement for that constructor + public AblyRest build() throws AblyException { + return new AblyRest(options); + } + } + + /** + * Builds a realtime client. Obtain one from {@link PubSubServer#realtimeClientBuilder()}. + */ + public static final class RealtimeClientBuilder extends ClientBuilder { + + RealtimeClientBuilder() { + } + + /** + * Sets {@link ClientOptions#realtimeHost}. + * + * @param realtimeHost the value to set. + * @return this builder. + */ + public RealtimeClientBuilder realtimeHost(String realtimeHost) { + options.realtimeHost = realtimeHost; + return this; + } + + /** + * Sets {@link ClientOptions#autoConnect}. + * + * @param autoConnect the value to set. + * @return this builder. + */ + public RealtimeClientBuilder autoConnect(boolean autoConnect) { + options.autoConnect = autoConnect; + return this; + } + + /** + * Sets {@link ClientOptions#queueMessages}. + * + * @param queueMessages the value to set. + * @return this builder. + */ + public RealtimeClientBuilder queueMessages(boolean queueMessages) { + options.queueMessages = queueMessages; + return this; + } + + /** + * Sets {@link ClientOptions#echoMessages}. + * + * @param echoMessages the value to set. + * @return this builder. + */ + public RealtimeClientBuilder echoMessages(boolean echoMessages) { + options.echoMessages = echoMessages; + return this; + } + + /** + * Sets {@link ClientOptions#recover}. + * + * @param recover the value to set. + * @return this builder. + */ + public RealtimeClientBuilder recover(String recover) { + options.recover = recover; + return this; + } + + /** + * Sets {@link ClientOptions#realtimeRequestTimeout}. + * + * @param realtimeRequestTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder realtimeRequestTimeout(long realtimeRequestTimeout) { + options.realtimeRequestTimeout = realtimeRequestTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#disconnectedRetryTimeout}. + * + * @param disconnectedRetryTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder disconnectedRetryTimeout(long disconnectedRetryTimeout) { + options.disconnectedRetryTimeout = disconnectedRetryTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#suspendedRetryTimeout}. + * + * @param suspendedRetryTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder suspendedRetryTimeout(long suspendedRetryTimeout) { + options.suspendedRetryTimeout = suspendedRetryTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#channelRetryTimeout}. + * + * @param channelRetryTimeout the value to set. + * @return this builder. + */ + public RealtimeClientBuilder channelRetryTimeout(int channelRetryTimeout) { + options.channelRetryTimeout = channelRetryTimeout; + return this; + } + + /** + * Sets {@link ClientOptions#transportParams}. + * + * @param transportParams the value to set. + * @return this builder. + */ + public RealtimeClientBuilder transportParams(Param[] transportParams) { + options.transportParams = transportParams; + return this; + } + + /** + * Builds the client, which connects immediately unless {@link #autoConnect(boolean)} was + * set to false. + * + * @return an {@link AblyRealtime}. + * @throws AblyException if the options are invalid, for example if no authentication + * parameters were supplied. + */ + @SuppressWarnings("deprecation") // this factory is the replacement for that constructor + public AblyRealtime build() throws AblyException { + return new AblyRealtime(options); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index dfd7150f4..24b1b2c80 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,8 @@ include("network-client-core") include("network-client-default") include("network-client-okhttp") include("pubsub-adapter") +include("pubsub-server") +include("pubsub-device") include("liveobjects") include("examples") include("uts")