Skip to content

Commit 61e1beb

Browse files
greenrobotgreenrobot-team
authored andcommitted
Add SyncHybrid; a combo of SyncClient and SyncServer
Useful for embedded cluster setups that also want a local DB for immediate persistence (like Sync clients).
1 parent 8c6cd97 commit 61e1beb

File tree

13 files changed

+371
-8
lines changed

13 files changed

+371
-8
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package io.objectbox.sync;
1818

1919
import io.objectbox.BoxStore;
20+
import io.objectbox.BoxStoreBuilder;
21+
import io.objectbox.sync.server.SyncHybridBuilder;
2022
import io.objectbox.sync.server.SyncServer;
2123
import io.objectbox.sync.server.SyncServerBuilder;
2224

@@ -42,6 +44,13 @@ public static boolean isServerAvailable() {
4244
return BoxStore.isSyncServerAvailable();
4345
}
4446

47+
/**
48+
* Returns true if the included native (JNI) ObjectBox library supports Sync hybrids (server & client).
49+
*/
50+
public static boolean isHybridAvailable() {
51+
return isAvailable() && isServerAvailable();
52+
}
53+
4554
/**
4655
* Start building a sync client. Requires the BoxStore that should be synced with the server,
4756
* the URL and port of the server to connect to and credentials to authenticate against the server.
@@ -67,6 +76,29 @@ public static SyncServerBuilder server(BoxStore boxStore, String url, SyncCreden
6776
return new SyncServerBuilder(boxStore, url, authenticatorCredentials);
6877
}
6978

79+
/**
80+
* Starts building a {@link SyncHybridBuilder}, a client/server hybrid typically used for embedded cluster setups.
81+
* <p/>
82+
* Unlike {@link #client(BoxStore, String, SyncCredentials)} and {@link #server(BoxStore, String, SyncCredentials)},
83+
* you cannot pass in an already built store. Instead, you must pass in the store builder.
84+
* The store will be created internally when calling this method.
85+
* <p/>
86+
* As this is a hybrid, you can configure client and server aspects using the {@link SyncHybridBuilder}.
87+
*
88+
* @param storeBuilder the BoxStoreBuilder to use for building the main store.
89+
* @param url The URL of the Sync server on which the Sync protocol is exposed. This is typically a WebSockets URL
90+
* starting with {@code ws://} or {@code wss://} (for encrypted connections), for example
91+
* {@code ws://0.0.0.0:9999}.
92+
* @param authenticatorCredentials A list of enabled authentication methods available to Sync clients. Additional
93+
* authenticator credentials can be supplied using the builder. For the embedded server, currently only
94+
* {@link SyncCredentials#sharedSecret} and {@link SyncCredentials#none} are supported.
95+
* @return an instance of SyncHybridBuilder.
96+
*/
97+
public static SyncHybridBuilder hybrid(BoxStoreBuilder storeBuilder, String url,
98+
SyncCredentials authenticatorCredentials) {
99+
return new SyncHybridBuilder(storeBuilder, url, authenticatorCredentials);
100+
}
101+
70102
private Sync() {
71103
}
72104
}

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

Lines changed: 19 additions & 4 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;
@@ -38,7 +39,7 @@ public 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,24 @@ 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+
public SyncBuilder(BoxStore boxStore, String url, SyncCredentials credentials) {
101+
this(boxStore, credentials);
102+
checkNotNull(url, "Sync server URL is required.");
103+
this.url = url;
104+
}
105+
106+
/**
107+
* Internal URL setter for late assignment (used by {@link io.objectbox.sync.server.SyncHybridBuilder}).
108+
*/
109+
@Internal
110+
public SyncBuilder lateUrl(String url) {
111+
this.url = url;
112+
return this;
113+
}
114+
100115
/**
101116
* Configures a custom set of directory or file paths to search for trusted certificates in.
102117
* The first path that exists will be used.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ public class SyncClientImpl implements SyncClient {
6060
private volatile boolean started;
6161

6262
SyncClientImpl(SyncBuilder builder) {
63+
if (builder.url == null) {
64+
throw new IllegalArgumentException("Sync client destination URL was not specified");
65+
}
66+
6367
this.boxStore = builder.boxStore;
6468
this.serverUrl = builder.url;
6569
this.connectivityMonitor = builder.platform.getConnectivityMonitor();

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+
public abstract SyncCredentials createClone();
96+
8997
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,15 @@ public void clear() {
7474
this.token = null;
7575
}
7676

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

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+
public SyncCredentials createClone() {
47+
return new SyncCredentialsUserPassword(this.username, this.password);
48+
}
4449
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.server;
18+
19+
import java.io.Closeable;
20+
21+
import io.objectbox.BoxStore;
22+
import io.objectbox.sync.SyncClient;
23+
24+
/**
25+
* The SyncHybrid combines the functionality of a Sync Client and a Sync Server.
26+
* It is typically used in local cluster setups, in which a "hybrid" functions as a client & cluster peer (server).
27+
* <p/>
28+
* Call {@link #getStore()} to retrieve the store.
29+
* To set sync listeners use the {@link SyncClient} that is available from {@link #getClient()}.
30+
* <p/>
31+
* This class implements the Closeable interface, ensuring that resources are cleaned up properly.
32+
*/
33+
public final class SyncHybrid implements Closeable {
34+
private BoxStore store;
35+
private final SyncClient client;
36+
private BoxStore storeServer;
37+
private final SyncServer server;
38+
39+
public SyncHybrid(BoxStore store, SyncClient client, BoxStore storeServer, SyncServer server) {
40+
this.store = store;
41+
this.client = client;
42+
this.storeServer = storeServer;
43+
this.server = server;
44+
}
45+
46+
public BoxStore getStore() {
47+
return store;
48+
}
49+
50+
/**
51+
* Typically only used to set sync listeners.
52+
* <p/>
53+
* Note: you should not directly call start(), stop(), close() on the {@link SyncClient} directly.
54+
* Instead, call {@link #stop()} or {@link #close()} on this instance (it is already started during creation).
55+
*/
56+
public SyncClient getClient() {
57+
return client;
58+
}
59+
60+
/**
61+
* Typically, you won't need access to the SyncServer.
62+
* It is still exposed for advanced use cases if you know what you are doing.
63+
* <p/>
64+
* Note: you should not directly call start(), stop(), close() on the {@link SyncServer} directly.
65+
* Instead, call {@link #stop()} or {@link #close()} on this instance (it is already started during creation).
66+
*/
67+
public SyncServer getServer() {
68+
return server;
69+
}
70+
71+
public void stop() {
72+
client.stop();
73+
server.stop();
74+
}
75+
76+
@Override
77+
public void close() {
78+
// Clear reference to boxStore but do not close it (same behavior as SyncClient and SyncServer)
79+
store = null;
80+
client.close();
81+
server.close();
82+
if (storeServer != null) {
83+
storeServer.close(); // The server store is "internal", so we can close it
84+
storeServer = null;
85+
}
86+
}
87+
88+
/**
89+
* Users of this class should explicitly call {@link #close()} instead to avoid expensive finalization.
90+
*/
91+
@SuppressWarnings("deprecation") // finalize()
92+
@Override
93+
protected void finalize() throws Throwable {
94+
close();
95+
super.finalize();
96+
}
97+
}

0 commit comments

Comments
 (0)