Skip to content

Commit 0805f1f

Browse files
SyncClient: support multiple URLs, move adding credentials to builder
1 parent 8e8a364 commit 0805f1f

8 files changed

Lines changed: 136 additions & 59 deletions

File tree

objectbox-java/src/main/java/io/objectbox/sync/Sync.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package io.objectbox.sync;
1818

19+
import java.util.Collections;
20+
import java.util.List;
21+
1922
import io.objectbox.BoxStore;
2023
import io.objectbox.BoxStoreBuilder;
2124
import io.objectbox.sync.server.SyncServer;
@@ -50,6 +53,43 @@ public static boolean isHybridAvailable() {
5053
return isAvailable() && isServerAvailable();
5154
}
5255

56+
/**
57+
* Starts building a {@link SyncClient} for the given {@link BoxStore} and server URL.
58+
* <p>
59+
* This does not initiate any connection attempts yet: call {@link SyncBuilder#buildAndStart()} to do so. Before,
60+
* you must configure credentials via {@link SyncBuilder#credentials(SyncCredentials)}.
61+
* <p>
62+
* By default, a Sync client automatically receives updates from the server once login succeeded. To configure this
63+
* differently, call {@link SyncBuilder#requestUpdatesMode(SyncBuilder.RequestUpdatesMode)} with the wanted mode.
64+
*
65+
* @param boxStore The {@link BoxStore} the client should use.
66+
* @param url The URL of the Sync server on which the Sync protocol is exposed. This is typically a WebSockets URL
67+
* starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
68+
* {@code ws://127.0.0.1:9999}.
69+
* @return a builder to configure the Sync client
70+
* @see #client(BoxStore, List)
71+
*/
72+
public static SyncBuilder client(BoxStore boxStore, String url) {
73+
return client(boxStore, Collections.singletonList(url));
74+
}
75+
76+
/**
77+
* Like {@link #client(BoxStore, String)}, but supports passing multiple URLs.
78+
* <p>
79+
* Passing multiple URLs allows high availability and load balancing (i.e. using an ObjectBox Sync Server Cluster).
80+
* <p>
81+
* A random URL is selected for each connection attempt.
82+
*
83+
* @param boxStore The {@link BoxStore} the client should use.
84+
* @param urls A list of URLs of Sync servers on which the Sync protocol is exposed. These are typically WebSockets
85+
* URLs starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
86+
* {@code ws://127.0.0.1:9999}.
87+
* @return a builder to configure the Sync client
88+
*/
89+
public static SyncBuilder client(BoxStore boxStore, List<String> urls) {
90+
return new SyncBuilder(boxStore, urls);
91+
}
92+
5393
/**
5494
* Starts building a {@link SyncClient}. Once done, complete with {@link SyncBuilder#build() build()}.
5595
*
@@ -58,18 +98,31 @@ public static boolean isHybridAvailable() {
5898
* starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
5999
* {@code ws://127.0.0.1:9999}.
60100
* @param credentials {@link SyncCredentials} to authenticate with the server.
101+
* @deprecated Use {@link #client(BoxStore, String)} and {@link SyncBuilder#credentials(SyncCredentials)} instead.
61102
*/
103+
@Deprecated
62104
public static SyncBuilder client(BoxStore boxStore, String url, SyncCredentials credentials) {
63-
return new SyncBuilder(boxStore, url, credentials);
105+
SyncBuilder builder = client(boxStore, url);
106+
builder.credentials(credentials);
107+
return builder;
64108
}
65109

66110
/**
67111
* Like {@link #client(BoxStore, String, SyncCredentials)}, but supports passing a set of authentication methods.
68112
*
69113
* @param multipleCredentials An array of {@link SyncCredentials} to be used to authenticate with the server.
114+
* @deprecated Use {@link #client(BoxStore, String)} and {@link SyncBuilder#credentials(SyncCredentials)} instead.
70115
*/
116+
@Deprecated
71117
public static SyncBuilder client(BoxStore boxStore, String url, SyncCredentials[] multipleCredentials) {
72-
return new SyncBuilder(boxStore, url, multipleCredentials);
118+
SyncBuilder builder = client(boxStore, url);
119+
//noinspection ConstantValue
120+
if (multipleCredentials != null) {
121+
for (SyncCredentials credentials : multipleCredentials) {
122+
builder.credentials(credentials);
123+
}
124+
}
125+
return builder;
73126
}
74127

75128
/**

objectbox-java/src/main/java/io/objectbox/sync/SyncBuilder.java

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package io.objectbox.sync;
1818

19+
import java.util.ArrayList;
1920
import java.util.Arrays;
20-
import java.util.Collections;
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.TreeMap;
@@ -44,8 +44,13 @@ public final class SyncBuilder {
4444

4545
final Platform platform;
4646
final BoxStore boxStore;
47-
@Nullable private String url;
48-
final List<SyncCredentials> credentials;
47+
/**
48+
* The server URLs this client may connect to.
49+
* <p>
50+
* See {@link Sync#client(BoxStore, List)} for notes on multiple URLs.
51+
*/
52+
final List<String> urls = new ArrayList<>();
53+
final List<SyncCredentials> credentials = new ArrayList<>();
4954

5055
@Nullable SyncLoginListener loginListener;
5156
@Nullable SyncCompletedListener completedListener;
@@ -98,49 +103,42 @@ private static void checkSyncFeatureAvailable() {
98103
}
99104
}
100105

101-
private SyncBuilder(BoxStore boxStore, @Nullable String url, @Nullable List<SyncCredentials> credentials) {
102-
checkNotNull(boxStore, "BoxStore is required.");
103-
checkNotNull(credentials, "Sync credentials are required.");
106+
/**
107+
* Creates a builder for a {@link SyncClient}.
108+
* <p>
109+
* Don't use this directly, use the {@link Sync#client} methods instead.
110+
*/
111+
SyncBuilder(BoxStore boxStore, List<String> urls) {
112+
checkNotNull(boxStore, "boxStore");
113+
checkNotNull(urls, "urls");
104114
this.boxStore = boxStore;
105-
this.url = url;
106-
this.credentials = credentials;
115+
// For SyncHybridBuilder, delay validating there is a URL until the build call
116+
for (String url : urls) {
117+
url(url);
118+
}
107119
checkSyncFeatureAvailable();
108120
this.platform = Platform.findPlatform(); // Requires APIs only present in Android Sync library
109121
}
110122

111-
@Internal
112-
public SyncBuilder(BoxStore boxStore, String url, @Nullable SyncCredentials credentials) {
113-
this(boxStore, url, credentials == null ? null : Collections.singletonList(credentials));
114-
}
115-
116-
@Internal
117-
public SyncBuilder(BoxStore boxStore, String url, @Nullable SyncCredentials[] multipleCredentials) {
118-
this(boxStore, url, multipleCredentials == null ? null : Arrays.asList(multipleCredentials));
119-
}
120-
121123
/**
122-
* When using this constructor, make sure to set the server URL before starting.
124+
* Allows internal code to set the Sync server URL after creating this builder.
123125
*/
124126
@Internal
125-
public SyncBuilder(BoxStore boxStore, @Nullable SyncCredentials credentials) {
126-
this(boxStore, null, credentials == null ? null : Collections.singletonList(credentials));
127+
SyncBuilder url(String url) {
128+
checkNotNull(url, "url");
129+
this.urls.add(url);
130+
return this;
127131
}
128132

129133
/**
130-
* Allows internal code to set the Sync server URL after creating this builder.
134+
* Adds {@link SyncCredentials} to authenticate the client with the server.
131135
*/
132-
@Internal
133-
SyncBuilder serverUrl(String url) {
134-
this.url = url;
136+
public SyncBuilder credentials(SyncCredentials credentials) {
137+
checkNotNull(credentials, "credentials");
138+
this.credentials.add(credentials);
135139
return this;
136140
}
137141

138-
@Internal
139-
String serverUrl() {
140-
checkNotNull(url, "Sync Server URL is null.");
141-
return url;
142-
}
143-
144142
/**
145143
* Adds or replaces a <a href="https://sync.objectbox.io/sync-server/sync-filters">Sync filter</a> variable value
146144
* for the given name.
@@ -151,8 +149,8 @@ String serverUrl() {
151149
* @see SyncClient#putFilterVariable
152150
*/
153151
public SyncBuilder filterVariable(String name, String value) {
154-
checkNotNull(name, "Filter variable name is null.");
155-
checkNotNull(value, "Filter variable value is null.");
152+
checkNotNull(name, "name");
153+
checkNotNull(value, "value");
156154
filterVariables.put(name, value);
157155
return this;
158156
}
@@ -265,23 +263,25 @@ public SyncClient build() {
265263
if (boxStore.getSyncClient() != null) {
266264
throw new IllegalStateException("The given store is already associated with a Sync client, close it first.");
267265
}
268-
checkNotNull(url, "Sync Server URL is required.");
269266
return new SyncClientImpl(this);
270267
}
271268

272269
/**
273-
* Builds, {@link SyncClient#start() starts} and returns a Sync client.
270+
* {@link #build() Builds}, {@link SyncClient#start() starts} and returns a Sync client.
274271
*/
275272
public SyncClient buildAndStart() {
276273
SyncClient syncClient = build();
277274
syncClient.start();
278275
return syncClient;
279276
}
280277

281-
private void checkNotNull(@Nullable Object object, String message) {
282-
//noinspection ConstantConditions Non-null annotation does not enforce, so check for null.
278+
/**
279+
* Nullness annotations are only a hint in Java, so explicitly check nonnull annotated parameters
280+
* (see package-info.java for package settings).
281+
*/
282+
private void checkNotNull(@Nullable Object object, String name) {
283283
if (object == null) {
284-
throw new IllegalArgumentException(message);
284+
throw new IllegalArgumentException(name + " must not be null.");
285285
}
286286
}
287287

objectbox-java/src/main/java/io/objectbox/sync/SyncClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package io.objectbox.sync;
1818

1919
import java.io.Closeable;
20+
import java.util.List;
2021

2122
import javax.annotation.Nullable;
2223

24+
import io.objectbox.BoxStore;
2325
import io.objectbox.annotation.apihint.Experimental;
2426
import io.objectbox.sync.SyncBuilder.RequestUpdatesMode;
2527
import io.objectbox.sync.listener.SyncChangeListener;
@@ -42,9 +44,19 @@ public interface SyncClient extends Closeable {
4244

4345
/**
4446
* Gets the sync server URL this client is connected to.
47+
*
48+
* @deprecated Use {@link #getUrls()}
4549
*/
50+
@Deprecated
4651
String getServerUrl();
4752

53+
/**
54+
* Gets the sync server URLs this client may connect to.
55+
* <p>
56+
* See {@link Sync#client(BoxStore, List)} for notes on multiple URLs.
57+
*/
58+
List<String> getUrls();
59+
4860
/**
4961
* Flag indicating if the sync client was started.
5062
* Started clients try to connect, login, and sync with the sync destination.

objectbox-java/src/main/java/io/objectbox/sync/SyncClientImpl.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.objectbox.sync;
1818

19+
import java.util.List;
1920
import java.util.Map;
2021
import java.util.concurrent.CountDownLatch;
2122
import java.util.concurrent.TimeUnit;
@@ -43,7 +44,7 @@ public final class SyncClientImpl implements SyncClient {
4344

4445
@Nullable
4546
private BoxStore boxStore;
46-
private final String serverUrl;
47+
private final List<String> urls;
4748
private final InternalSyncClientListener internalListener;
4849
@Nullable
4950
private final ConnectivityMonitor connectivityMonitor;
@@ -62,7 +63,7 @@ public final class SyncClientImpl implements SyncClient {
6263

6364
SyncClientImpl(SyncBuilder builder) {
6465
this.boxStore = builder.boxStore;
65-
this.serverUrl = builder.serverUrl();
66+
this.urls = builder.urls;
6667
this.connectivityMonitor = builder.platform.getConnectivityMonitor();
6768

6869
// Build the options
@@ -71,8 +72,10 @@ public final class SyncClientImpl implements SyncClient {
7172
throw new RuntimeException("Failed to create Sync client options: handle is zero.");
7273
}
7374
try {
74-
// Add server URL
75-
nativeSyncOptAddUrl(optHandle, serverUrl);
75+
// Add all server URLs
76+
for (String url : urls) {
77+
nativeSyncOptAddUrl(optHandle, url);
78+
}
7679

7780
// Add trusted certificate paths if provided
7881
if (builder.trustedCertPaths != null) {
@@ -144,7 +147,13 @@ private long getHandle() {
144147

145148
@Override
146149
public String getServerUrl() {
147-
return serverUrl;
150+
// nativeSyncOptCreateClient guarantees there is at least one URL
151+
return getUrls().get(0);
152+
}
153+
154+
@Override
155+
public List<String> getUrls() {
156+
return urls;
148157
}
149158

150159
@Override

objectbox-java/src/main/java/io/objectbox/sync/SyncHybridBuilder.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package io.objectbox.sync;
1818

19+
import java.util.Collections;
20+
1921
import io.objectbox.BoxStore;
2022
import io.objectbox.BoxStoreBuilder;
2123
import io.objectbox.InternalAccess;
@@ -46,7 +48,7 @@ public final class SyncHybridBuilder {
4648
boxStore = storeBuilder.build();
4749
boxStoreServer = storeBuilderServer.build();
4850
SyncCredentials clientCredentials = authenticatorCredentials.createClone();
49-
clientBuilder = new SyncBuilder(boxStore, clientCredentials); // Do not yet set URL, port may be dynamic
51+
clientBuilder = new SyncBuilder(boxStore, Collections.emptyList()).credentials(clientCredentials); // Do not yet set URL, port may be dynamic
5052
serverBuilder = new SyncServerBuilder(boxStoreServer, url, authenticatorCredentials);
5153
}
5254

@@ -75,7 +77,7 @@ public SyncHybrid buildAndStart() {
7577
SyncServer server = serverBuilder.buildAndStart();
7678

7779
SyncClient client = clientBuilder
78-
.serverUrl(server.getUrl())
80+
.url(server.getUrl())
7981
.buildAndStart();
8082

8183
return new SyncHybrid(boxStore, client, boxStoreServer, server);

objectbox-java/src/main/java/io/objectbox/sync/package-info.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,15 @@
1818
* <a href="https://objectbox.io/sync/">ObjectBox Sync</a> allows to automatically synchronize local data with a sync
1919
* destination (e.g. a sync server) and vice versa. This is the sync <b>client</b> package.
2020
* <p>
21-
* These are the typical steps to setup a sync client:
21+
* These are the typical steps to set up a sync client:
2222
* <ol>
2323
* <li>Create a BoxStore as usual (using MyObjectBox).</li>
24-
* <li>Get a {@link io.objectbox.sync.SyncBuilder} using
25-
* {@link io.objectbox.sync.Sync#client(io.objectbox.BoxStore, java.lang.String, io.objectbox.sync.SyncCredentials) Sync.client(boxStore, url, credentials)}.
26-
* Here you need to pass the {@link io.objectbox.BoxStore BoxStore}, along with an URL to the sync destination (server),
27-
* and credentials. For demo set ups, you could start with {@link io.objectbox.sync.SyncCredentials#none()}
28-
* credentials.</li>
24+
* <li>Build a {@link io.objectbox.sync.SyncClient} using
25+
* {@link io.objectbox.sync.Sync#client(io.objectbox.BoxStore, java.lang.String) Sync.client(boxStore, url)} and at
26+
* least one set of credentials with {@link io.objectbox.sync.SyncBuilder#credentials(io.objectbox.sync.SyncCredentials)}.</li>
2927
* <li>Optional: use the {@link io.objectbox.sync.SyncBuilder} instance from the last step to configure the sync
3028
* client and set initial listeners.</li>
31-
* <li>Call {@link io.objectbox.sync.SyncBuilder#build()} to get an instance of
29+
* <li>Call {@link io.objectbox.sync.SyncBuilder#buildAndStart()} to get an instance of
3230
* {@link io.objectbox.sync.SyncClient} (and hold on to it). Synchronization is now active.</li>
3331
* <li>Optional: Interact with {@link io.objectbox.sync.SyncClient}.</li>
3432
* </ol>

tests/objectbox-java-test/src/test/java/io/objectbox/sync/ConnectivityMonitorTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import org.junit.Test;
2020

21+
import java.util.List;
22+
2123
import javax.annotation.Nullable;
2224

2325
import io.objectbox.sync.listener.SyncChangeListener;
@@ -114,6 +116,11 @@ public String getServerUrl() {
114116
return null;
115117
}
116118

119+
@Override
120+
public List<String> getUrls() {
121+
return null;
122+
}
123+
117124
@Override
118125
public boolean isStarted() {
119126
return false;

tests/objectbox-java-test/src/test/java/io/objectbox/sync/SyncTest.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,10 @@ public void serverIsNotAvailable() {
5757

5858
@Test
5959
public void creatingSyncClient_throws() {
60-
// If no credentials are passed
61-
assertThrows(IllegalArgumentException.class, () -> Sync.client(store, SERVER_URL, (SyncCredentials) null));
62-
assertThrows(IllegalArgumentException.class, () -> Sync.client(store, SERVER_URL, (SyncCredentials[]) null));
63-
6460
// If no Sync feature is available
6561
FeatureNotAvailableException exception = assertThrows(
6662
FeatureNotAvailableException.class,
67-
() -> Sync.client(store, SERVER_URL, SyncCredentials.none())
63+
() -> Sync.client(store, SERVER_URL)
6864
);
6965
String message = exception.getMessage();
7066
assertTrue(message, message.contains("does not include ObjectBox Sync") &&

0 commit comments

Comments
 (0)