Skip to content

Commit 6a3fb19

Browse files
Merge branch '220-cluster-hybrid' into 'dev'
SyncHybrid (Server and Client) #220 See merge request objectbox/objectbox-java!144
2 parents 8c6cd97 + 39b3f9a commit 6a3fb19

File tree

15 files changed

+406
-26
lines changed

15 files changed

+406
-26
lines changed

objectbox-java/src/main/java/io/objectbox/BoxStoreBuilder.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,4 +682,43 @@ public BoxStore buildDefault() {
682682
BoxStore.setDefault(store);
683683
return store;
684684
}
685-
}
685+
686+
687+
@Internal
688+
BoxStoreBuilder createClone(String namePostfix) {
689+
if (model == null) {
690+
throw new IllegalStateException("BoxStoreBuilder must have a model");
691+
}
692+
if (initialDbFileFactory != null) {
693+
throw new IllegalStateException("Initial DB files factories are not supported for sync-enabled DBs");
694+
}
695+
696+
BoxStoreBuilder clone = new BoxStoreBuilder(model);
697+
// Note: don't use absolute path for directories; it messes with in-memory paths ("memory:")
698+
clone.directory = this.directory != null ? new File(this.directory.getPath() + namePostfix) : null;
699+
clone.baseDirectory = this.baseDirectory != null ? new File(this.baseDirectory.getPath()) : null;
700+
clone.name = this.name != null ? name + namePostfix : null;
701+
clone.inMemory = this.inMemory;
702+
clone.maxSizeInKByte = this.maxSizeInKByte;
703+
clone.maxDataSizeInKByte = this.maxDataSizeInKByte;
704+
clone.context = this.context;
705+
clone.relinker = this.relinker;
706+
clone.debugFlags = this.debugFlags;
707+
clone.debugRelations = this.debugRelations;
708+
clone.fileMode = this.fileMode;
709+
clone.maxReaders = this.maxReaders;
710+
clone.noReaderThreadLocals = this.noReaderThreadLocals;
711+
clone.queryAttempts = this.queryAttempts;
712+
clone.skipReadSchema = this.skipReadSchema;
713+
clone.readOnly = this.readOnly;
714+
clone.usePreviousCommit = this.usePreviousCommit;
715+
clone.validateOnOpenModePages = this.validateOnOpenModePages;
716+
clone.validateOnOpenPageLimit = this.validateOnOpenPageLimit;
717+
clone.validateOnOpenModeKv = this.validateOnOpenModeKv;
718+
719+
clone.initialDbFileFactory = this.initialDbFileFactory;
720+
clone.entityInfoList.addAll(this.entityInfoList); // Entity info is stateless & immutable; shallow clone is OK
721+
722+
return clone;
723+
}
724+
}

objectbox-java/src/main/java/io/objectbox/InternalAccess.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,9 @@ public static void enableCreationStackTracking() {
7878
Transaction.TRACK_CREATION_STACK = true;
7979
Cursor.TRACK_CREATION_STACK = true;
8080
}
81+
82+
@Internal
83+
public static BoxStoreBuilder clone(BoxStoreBuilder original, String namePostfix) {
84+
return original.createClone(namePostfix);
85+
}
8186
}

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

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
package io.objectbox.sync;
1818

1919
import io.objectbox.BoxStore;
20+
import io.objectbox.BoxStoreBuilder;
2021
import io.objectbox.sync.server.SyncServer;
2122
import io.objectbox.sync.server.SyncServerBuilder;
2223

2324
/**
2425
* <a href="https://objectbox.io/sync/">ObjectBox Sync</a> makes data available on other devices.
25-
* Start building a sync client using Sync.{@link #client(BoxStore, String, SyncCredentials)}
26-
* or an embedded server using Sync.{@link #server(BoxStore, String, SyncCredentials)}.
26+
* <p>
27+
* Use the static methods to build a Sync client or embedded server.
2728
*/
2829
@SuppressWarnings({"unused", "WeakerAccess"})
2930
public final class Sync {
@@ -43,8 +44,20 @@ public static boolean isServerAvailable() {
4344
}
4445

4546
/**
46-
* Start building a sync client. Requires the BoxStore that should be synced with the server,
47-
* the URL and port of the server to connect to and credentials to authenticate against the server.
47+
* Returns true if the included native (JNI) ObjectBox library supports Sync hybrids (server & client).
48+
*/
49+
public static boolean isHybridAvailable() {
50+
return isAvailable() && isServerAvailable();
51+
}
52+
53+
/**
54+
* Starts building a {@link SyncClient}. Once done, complete with {@link SyncBuilder#build() build()}.
55+
*
56+
* @param boxStore The {@link BoxStore} the client should use.
57+
* @param url The URL of the Sync server on which the Sync protocol is exposed. This is typically a WebSockets URL
58+
* starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
59+
* {@code ws://127.0.0.1:9999}.
60+
* @param credentials {@link SyncCredentials} to authenticate with the server.
4861
*/
4962
public static SyncBuilder client(BoxStore boxStore, String url, SyncCredentials credentials) {
5063
return new SyncBuilder(boxStore, url, credentials);
@@ -59,14 +72,38 @@ public static SyncBuilder client(BoxStore boxStore, String url, SyncCredentials
5972
* @param url The URL of the Sync server on which the Sync protocol is exposed. This is typically a WebSockets URL
6073
* starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
6174
* {@code ws://0.0.0.0:9999}.
62-
* @param authenticatorCredentials A list of enabled authentication methods available to Sync clients. Additional
63-
* authenticator credentials can be supplied using the builder. For the embedded server, currently only
75+
* @param authenticatorCredentials An authentication method available to Sync clients and peers. Additional
76+
* authenticator credentials can be supplied using the returned builder. For the embedded server, currently only
6477
* {@link SyncCredentials#sharedSecret} and {@link SyncCredentials#none} are supported.
6578
*/
6679
public static SyncServerBuilder server(BoxStore boxStore, String url, SyncCredentials authenticatorCredentials) {
6780
return new SyncServerBuilder(boxStore, url, authenticatorCredentials);
6881
}
6982

83+
/**
84+
* Starts building a {@link SyncHybrid}, a client/server hybrid typically used for embedded cluster setups.
85+
* <p>
86+
* Unlike {@link #client(BoxStore, String, SyncCredentials)} and {@link #server(BoxStore, String, SyncCredentials)},
87+
* the client Store is not built before. Instead, a Store builder must be passed. The client and server Store will
88+
* be built internally when calling this method.
89+
* <p>
90+
* To configure client and server use the methods on {@link SyncHybridBuilder}.
91+
*
92+
* @param storeBuilder The {@link BoxStoreBuilder} to use for building the client store.
93+
* @param url The URL of the Sync server on which the Sync protocol is exposed. This is typically a WebSockets URL
94+
* starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
95+
* {@code ws://0.0.0.0:9999}.
96+
* @param authenticatorCredentials An authentication method available to Sync clients and peers. The client of the
97+
* hybrid is pre-configured with them. Additional credentials can be supplied using the client and server builder of
98+
* the returned builder. For the embedded server, currently only {@link SyncCredentials#sharedSecret} and
99+
* {@link SyncCredentials#none} are supported.
100+
* @return An instance of {@link SyncHybridBuilder}.
101+
*/
102+
public static SyncHybridBuilder hybrid(BoxStoreBuilder storeBuilder, String url,
103+
SyncCredentials authenticatorCredentials) {
104+
return new SyncHybridBuilder(storeBuilder, url, authenticatorCredentials);
105+
}
106+
70107
private Sync() {
71108
}
72109
}

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import javax.annotation.Nullable;
2222

2323
import io.objectbox.BoxStore;
24+
import io.objectbox.annotation.apihint.Internal;
2425
import io.objectbox.sync.internal.Platform;
2526
import io.objectbox.sync.listener.SyncChangeListener;
2627
import io.objectbox.sync.listener.SyncCompletedListener;
@@ -34,11 +35,11 @@
3435
* {@link Sync#client(BoxStore, String, SyncCredentials)}.
3536
*/
3637
@SuppressWarnings({"unused", "WeakerAccess"})
37-
public class SyncBuilder {
38+
public final class SyncBuilder {
3839

3940
final Platform platform;
4041
final BoxStore boxStore;
41-
final String url;
42+
String url;
4243
final SyncCredentials credentials;
4344

4445
@Nullable SyncLoginListener loginListener;
@@ -82,9 +83,9 @@ public enum RequestUpdatesMode {
8283
AUTO_NO_PUSHES
8384
}
8485

85-
public SyncBuilder(BoxStore boxStore, String url, SyncCredentials credentials) {
86+
@Internal
87+
public SyncBuilder(BoxStore boxStore, SyncCredentials credentials) {
8688
checkNotNull(boxStore, "BoxStore is required.");
87-
checkNotNull(url, "Sync server URL is required.");
8889
checkNotNull(credentials, "Sync credentials are required.");
8990
if (!BoxStore.isSyncAvailable()) {
9091
throw new IllegalStateException(
@@ -93,10 +94,25 @@ public SyncBuilder(BoxStore boxStore, String url, SyncCredentials credentials) {
9394
}
9495
this.platform = Platform.findPlatform();
9596
this.boxStore = boxStore;
96-
this.url = url;
9797
this.credentials = credentials;
9898
}
9999

100+
@Internal
101+
public SyncBuilder(BoxStore boxStore, String url, SyncCredentials credentials) {
102+
this(boxStore, credentials);
103+
checkNotNull(url, "Sync server URL is required.");
104+
this.url = url;
105+
}
106+
107+
/**
108+
* Allows internal code to set the Sync server URL after creating this builder.
109+
*/
110+
@Internal
111+
SyncBuilder serverUrl(String url) {
112+
this.url = url;
113+
return this;
114+
}
115+
100116
/**
101117
* Configures a custom set of directory or file paths to search for trusted certificates in.
102118
* The first path that exists will be used.
@@ -205,6 +221,7 @@ public SyncClient build() {
205221
if (boxStore.getSyncClient() != null) {
206222
throw new IllegalStateException("The given store is already associated with a Sync client, close it first.");
207223
}
224+
checkNotNull(url, "Sync Server URL is required.");
208225
return new SyncClientImpl(this);
209226
}
210227

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
* this class may change without notice.
3939
*/
4040
@Internal
41-
public class SyncClientImpl implements SyncClient {
41+
public final class SyncClientImpl implements SyncClient {
4242

4343
@Nullable
4444
private BoxStore boxStore;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
* for example {@link #sharedSecret(String) SyncCredentials.sharedSecret("secret")}.
2222
*/
2323
@SuppressWarnings("unused")
24-
public class SyncCredentials {
24+
public abstract class SyncCredentials {
2525

2626
private final CredentialsType type;
2727

@@ -86,4 +86,12 @@ public long getTypeId() {
8686
return type.id;
8787
}
8888

89+
/**
90+
* Creates a copy of these credentials.
91+
* <p>
92+
* This can be useful to use the same credentials when creating multiple clients or a server in combination with a
93+
* client as some credentials may get cleared when building a client or server.
94+
*/
95+
abstract SyncCredentials createClone();
96+
8997
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ public byte[] getTokenBytes() {
6262
/**
6363
* Clear after usage.
6464
* <p>
65-
* Note that actual data is not removed from memory until the next garbage collector run.
66-
* Anyhow, the credentials are still kept in memory by the native component.
65+
* Note that when the token is passed as a String, that String is removed from memory at the earliest with the next
66+
* garbage collector run.
67+
* <p>
68+
* Also note that while the token is removed from the Java heap, it is present on the native heap of the Sync
69+
* component using it.
6770
*/
6871
public void clear() {
6972
cleared = true;
@@ -74,4 +77,15 @@ public void clear() {
7477
this.token = null;
7578
}
7679

80+
@Override
81+
SyncCredentialsToken createClone() {
82+
if (cleared) {
83+
throw new IllegalStateException("Cannot clone: credentials already have been cleared");
84+
}
85+
if (token == null) {
86+
return new SyncCredentialsToken(getType());
87+
} else {
88+
return new SyncCredentialsToken(getType(), Arrays.copyOf(token, token.length));
89+
}
90+
}
7791
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ public String getUsername() {
4141
public String getPassword() {
4242
return password;
4343
}
44+
45+
@Override
46+
SyncCredentials createClone() {
47+
return new SyncCredentialsUserPassword(this.username, this.password);
48+
}
4449
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2024 ObjectBox Ltd. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.objectbox.sync;
18+
19+
import java.io.Closeable;
20+
21+
import io.objectbox.BoxStore;
22+
import io.objectbox.sync.server.SyncServer;
23+
24+
/**
25+
* Combines the functionality of a Sync client and a Sync server.
26+
* <p>
27+
* It is typically used in local cluster setups, in which a "hybrid" functions as a client & cluster peer (server).
28+
* <p>
29+
* Call {@link #getStore()} to retrieve the store. To set sync listeners use the {@link SyncClient} that is available
30+
* from {@link #getClient()}.
31+
* <p>
32+
* This class implements the {@link Closeable} interface, ensuring that resources are cleaned up properly.
33+
*/
34+
public final class SyncHybrid implements Closeable {
35+
private BoxStore store;
36+
private final SyncClient client;
37+
private BoxStore storeServer;
38+
private final SyncServer server;
39+
40+
SyncHybrid(BoxStore store, SyncClient client, BoxStore storeServer, SyncServer server) {
41+
this.store = store;
42+
this.client = client;
43+
this.storeServer = storeServer;
44+
this.server = server;
45+
}
46+
47+
public BoxStore getStore() {
48+
return store;
49+
}
50+
51+
/**
52+
* Returns the {@link SyncClient} of this hybrid, typically only to set Sync listeners.
53+
* <p>
54+
* Note: do not stop or close the client directly. Instead, use the {@link #stop()} and {@link #close()} methods of
55+
* this hybrid.
56+
*/
57+
public SyncClient getClient() {
58+
return client;
59+
}
60+
61+
/**
62+
* Returns the {@link SyncServer} of this hybrid.
63+
* <p>
64+
* Typically, the server should not be touched. Yet, it is still exposed for advanced use cases.
65+
* <p>
66+
* Note: do not stop or close the server directly. Instead, use the {@link #stop()} and {@link #close()} methods of
67+
* this hybrid.
68+
*/
69+
public SyncServer getServer() {
70+
return server;
71+
}
72+
73+
/**
74+
* Stops the client and server.
75+
*/
76+
public void stop() {
77+
client.stop();
78+
server.stop();
79+
}
80+
81+
/**
82+
* Closes and cleans up all resources used by this Sync hybrid.
83+
* <p>
84+
* It can no longer be used afterward, build a new one instead.
85+
* <p>
86+
* Does nothing if this has already been closed.
87+
*/
88+
@Override
89+
public void close() {
90+
// Clear reference to boxStore but do not close it (same behavior as SyncClient and SyncServer)
91+
store = null;
92+
client.close();
93+
server.close();
94+
if (storeServer != null) {
95+
storeServer.close(); // The server store is "internal", so can safely close it
96+
storeServer = null;
97+
}
98+
}
99+
100+
/**
101+
* Users of this class should explicitly call {@link #close()} instead to avoid expensive finalization.
102+
*/
103+
@SuppressWarnings("deprecation") // finalize()
104+
@Override
105+
protected void finalize() throws Throwable {
106+
close();
107+
super.finalize();
108+
}
109+
}

0 commit comments

Comments
 (0)