mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 03:09:04 +01:00
Initial implementation of client versioning client.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.api.sync;
|
||||
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ClientVersioningManager {
|
||||
|
||||
/**
|
||||
* The unique ID of the versioning client.
|
||||
*/
|
||||
ClientId CLIENT_ID = new ClientId("org.briarproject.bramble.versioning");
|
||||
|
||||
/**
|
||||
* The current version of the versioning client.
|
||||
*/
|
||||
int CLIENT_VERSION = 0;
|
||||
|
||||
void registerClient(ClientId clientId, int clientVersion);
|
||||
|
||||
void registerClientVersioningHook(ClientId clientId, int clientVersion,
|
||||
ClientVersioningHook hook);
|
||||
|
||||
interface ClientVersioningHook {
|
||||
|
||||
void onClientVisibilityChanging(Transaction txn, Contact c,
|
||||
Visibility v) throws DbException;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@ public interface SyncConstants {
|
||||
*/
|
||||
int MAX_RECORD_PAYLOAD_LENGTH = 48 * 1024; // 48 KiB
|
||||
|
||||
/** The maximum length of a group descriptor in bytes. */
|
||||
/**
|
||||
* The maximum length of a group descriptor in bytes.
|
||||
*/
|
||||
int MAX_GROUP_DESCRIPTOR_LENGTH = 16 * 1024; // 16 KiB
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.briarproject.bramble.sync;
|
||||
|
||||
interface ClientVersioningConstants {
|
||||
|
||||
// Metadata keys
|
||||
String MSG_KEY_UPDATE_VERSION = "version";
|
||||
String MSG_KEY_LOCAL = "local";
|
||||
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
package org.briarproject.bramble.sync;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.client.ContactGroupFactory;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.lifecycle.Service;
|
||||
import org.briarproject.bramble.api.lifecycle.ServiceException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Client;
|
||||
import org.briarproject.bramble.api.sync.ClientId;
|
||||
import org.briarproject.bramble.api.sync.ClientVersioningManager;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Group.Visibility;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||
import static org.briarproject.bramble.sync.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.bramble.sync.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.bramble.sync.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||
|
||||
@NotNullByDefault
|
||||
class ClientVersioningManagerImpl implements ClientVersioningManager, Client,
|
||||
Service, ContactHook, IncomingMessageHook {
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final ClientHelper clientHelper;
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final Clock clock;
|
||||
private final Group localGroup;
|
||||
|
||||
private final Collection<ClientVersion> clients =
|
||||
new CopyOnWriteArrayList<>();
|
||||
private final Map<ClientVersion, ClientVersioningHook> hooks =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@Inject
|
||||
ClientVersioningManagerImpl(DatabaseComponent db,
|
||||
ClientHelper clientHelper, ContactGroupFactory contactGroupFactory,
|
||||
Clock clock) {
|
||||
this.db = db;
|
||||
this.clientHelper = clientHelper;
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.clock = clock;
|
||||
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID,
|
||||
CLIENT_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerClient(ClientId clientId, int clientVersion) {
|
||||
clients.add(new ClientVersion(clientId, clientVersion));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerClientVersioningHook(ClientId clientId,
|
||||
int clientVersion, ClientVersioningHook hook) {
|
||||
hooks.put(new ClientVersion(clientId, clientVersion), hook);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLocalState(Transaction txn) throws DbException {
|
||||
if (db.containsGroup(txn, localGroup.getId())) return;
|
||||
db.addGroup(txn, localGroup);
|
||||
// Set things up for any pre-existing contacts
|
||||
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startService() throws ServiceException {
|
||||
List<ClientVersion> versions = new ArrayList<>(clients);
|
||||
Collections.sort(versions);
|
||||
try {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
if (updateClientVersions(txn, versions)) {
|
||||
for (Contact c : db.getContacts(txn))
|
||||
clientVersionsUpdated(txn, c, versions);
|
||||
}
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
throw new ServiceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopService() throws ServiceException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||
// Create a group and share it with the contact
|
||||
Group g = getContactGroup(c);
|
||||
db.addGroup(txn, g);
|
||||
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||
try {
|
||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
// Create and store the first local update
|
||||
List<ClientVersion> versions = new ArrayList<>(clients);
|
||||
Collections.sort(versions);
|
||||
storeFirstUpdate(txn, g.getId(), versions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
|
||||
throws DbException, InvalidMessageException {
|
||||
try {
|
||||
// Parse the new remote update
|
||||
Update newRemoteUpdate = parseUpdate(clientHelper.toList(m));
|
||||
List<ClientState> newRemoteStates = newRemoteUpdate.states;
|
||||
long newRemoteUpdateVersion = newRemoteUpdate.updateVersion;
|
||||
// Find the latest local and remote updates, if any
|
||||
LatestUpdates latest = findLatestUpdates(txn, m.getGroupId());
|
||||
// If this update is obsolete, delete it and return
|
||||
if (latest.remote != null
|
||||
&& latest.remote.updateVersion > newRemoteUpdateVersion) {
|
||||
db.deleteMessage(txn, m.getId());
|
||||
db.deleteMessageMetadata(txn, m.getId());
|
||||
return false;
|
||||
}
|
||||
// Load and parse the latest local update
|
||||
if (latest.local == null) throw new DbException();
|
||||
Update oldLocalUpdate = loadUpdate(txn, latest.local.messageId);
|
||||
List<ClientState> oldLocalStates = oldLocalUpdate.states;
|
||||
long oldLocalUpdateVersion = oldLocalUpdate.updateVersion;
|
||||
// Load and parse the previous remote update, if any
|
||||
List<ClientState> oldRemoteStates;
|
||||
if (latest.remote == null) {
|
||||
oldRemoteStates = emptyList();
|
||||
} else {
|
||||
oldRemoteStates =
|
||||
loadUpdate(txn, latest.remote.messageId).states;
|
||||
// Delete the previous remote update
|
||||
db.deleteMessage(txn, latest.remote.messageId);
|
||||
db.deleteMessageMetadata(txn, latest.remote.messageId);
|
||||
}
|
||||
// Update the local states from the remote states if necessary
|
||||
List<ClientState> newLocalStates = updateStatesFromRemoteStates(
|
||||
oldLocalStates, newRemoteStates);
|
||||
if (!oldLocalStates.equals(newLocalStates)) {
|
||||
// Delete the latest local update
|
||||
db.deleteMessage(txn, latest.local.messageId);
|
||||
db.deleteMessageMetadata(txn, latest.local.messageId);
|
||||
// Store a new local update
|
||||
storeUpdate(txn, m.getGroupId(), newLocalStates,
|
||||
oldLocalUpdateVersion + 1);
|
||||
}
|
||||
// Calculate the old and new client visibilities
|
||||
Map<ClientVersion, Visibility> before =
|
||||
getVisibilities(oldLocalStates, oldRemoteStates);
|
||||
Map<ClientVersion, Visibility> after =
|
||||
getVisibilities(newLocalStates, newRemoteStates);
|
||||
// Call hooks for any visibilities that have changed
|
||||
Contact c = getContact(txn, m.getGroupId());
|
||||
callVisibilityHooks(txn, c, before, after);
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void storeClientVersions(Transaction txn,
|
||||
List<ClientVersion> versions) throws DbException {
|
||||
long now = clock.currentTimeMillis();
|
||||
BdfList body = encodeClientVersions(versions);
|
||||
try {
|
||||
Message m = clientHelper.createMessage(localGroup.getId(), now,
|
||||
body);
|
||||
db.addLocalMessage(txn, m, new Metadata(), false);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private BdfList encodeClientVersions(List<ClientVersion> versions) {
|
||||
BdfList encoded = new BdfList();
|
||||
for (ClientVersion cv : versions)
|
||||
encoded.add(BdfList.of(cv.clientId.getString(), cv.clientVersion));
|
||||
return encoded;
|
||||
}
|
||||
|
||||
private boolean updateClientVersions(Transaction txn,
|
||||
List<ClientVersion> newVersions) throws DbException {
|
||||
Collection<MessageId> ids = db.getMessageIds(txn, localGroup.getId());
|
||||
if (ids.isEmpty()) {
|
||||
storeClientVersions(txn, newVersions);
|
||||
return true;
|
||||
}
|
||||
MessageId m = ids.iterator().next();
|
||||
List<ClientVersion> oldVersions = loadClientVersions(txn, m);
|
||||
if (oldVersions.equals(newVersions)) return false;
|
||||
db.removeMessage(txn, m);
|
||||
storeClientVersions(txn, newVersions);
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<ClientVersion> loadClientVersions(Transaction txn, MessageId m)
|
||||
throws DbException {
|
||||
try {
|
||||
BdfList body = clientHelper.getMessageAsList(txn, m);
|
||||
if (body == null) throw new DbException();
|
||||
return parseClientVersions(body);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ClientVersion> parseClientVersions(BdfList body)
|
||||
throws FormatException {
|
||||
int size = body.size();
|
||||
List<ClientVersion> parsed = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
BdfList cv = body.getList(i);
|
||||
ClientId clientId = new ClientId(cv.getString(0));
|
||||
int clientVersion = cv.getLong(1).intValue();
|
||||
parsed.add(new ClientVersion(clientId, clientVersion));
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private void clientVersionsUpdated(Transaction txn, Contact c,
|
||||
List<ClientVersion> versions) throws DbException {
|
||||
try {
|
||||
// Find the latest local and remote updates
|
||||
Group g = getContactGroup(c);
|
||||
LatestUpdates latest = findLatestUpdates(txn, g.getId());
|
||||
// Load and parse the latest local update
|
||||
if (latest.local == null) throw new DbException();
|
||||
Update oldLocalUpdate = loadUpdate(txn, latest.local.messageId);
|
||||
List<ClientState> oldLocalStates = oldLocalUpdate.states;
|
||||
long oldLocalUpdateVersion = oldLocalUpdate.updateVersion;
|
||||
// Delete the latest local update
|
||||
db.deleteMessage(txn, latest.local.messageId);
|
||||
db.deleteMessageMetadata(txn, latest.local.messageId);
|
||||
// Store a new local update
|
||||
List<ClientState> newLocalStates =
|
||||
updateStatesFromLocalVersions(oldLocalStates, versions);
|
||||
storeUpdate(txn, g.getId(), newLocalStates,
|
||||
oldLocalUpdateVersion + 1);
|
||||
// Load and parse the latest remote update, if any
|
||||
List<ClientState> remoteStates;
|
||||
if (latest.remote == null) remoteStates = emptyList();
|
||||
else remoteStates = loadUpdate(txn, latest.remote.messageId).states;
|
||||
// Calculate the old and new client visibilities
|
||||
Map<ClientVersion, Visibility> before =
|
||||
getVisibilities(oldLocalStates, remoteStates);
|
||||
Map<ClientVersion, Visibility> after =
|
||||
getVisibilities(newLocalStates, remoteStates);
|
||||
// Call hooks for any visibilities that have changed
|
||||
callVisibilityHooks(txn, c, before, after);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Group getContactGroup(Contact c) {
|
||||
return contactGroupFactory.createContactGroup(CLIENT_ID,
|
||||
CLIENT_VERSION, c);
|
||||
}
|
||||
|
||||
private LatestUpdates findLatestUpdates(Transaction txn, GroupId g)
|
||||
throws DbException, FormatException {
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
LatestUpdate local = null, remote = null;
|
||||
for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
|
||||
BdfDictionary meta = e.getValue();
|
||||
long updateVersion = meta.getLong(MSG_KEY_UPDATE_VERSION);
|
||||
if (meta.getBoolean(MSG_KEY_LOCAL))
|
||||
local = new LatestUpdate(e.getKey(), updateVersion);
|
||||
else remote = new LatestUpdate(e.getKey(), updateVersion);
|
||||
}
|
||||
return new LatestUpdates(local, remote);
|
||||
}
|
||||
|
||||
private Update loadUpdate(Transaction txn, MessageId m) throws DbException {
|
||||
try {
|
||||
BdfList body = clientHelper.getMessageAsList(txn, m);
|
||||
if (body == null) throw new DbException();
|
||||
return parseUpdate(body);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Update parseUpdate(BdfList body) throws FormatException {
|
||||
List<ClientState> states = parseClientStates(body);
|
||||
long updateVersion = parseUpdateVersion(body);
|
||||
return new Update(states, updateVersion);
|
||||
}
|
||||
|
||||
private List<ClientState> parseClientStates(BdfList body)
|
||||
throws FormatException {
|
||||
// Client states, update version
|
||||
BdfList states = body.getList(0);
|
||||
int size = states.size();
|
||||
List<ClientState> parsed = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++)
|
||||
parsed.add(parseClientState(states.getList(i)));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private ClientState parseClientState(BdfList clientState)
|
||||
throws FormatException {
|
||||
// Client ID, client version, active
|
||||
ClientId clientId = new ClientId(clientState.getString(0));
|
||||
int clientVersion = clientState.getLong(1).intValue();
|
||||
boolean active = clientState.getBoolean(2);
|
||||
return new ClientState(clientId, clientVersion, active);
|
||||
}
|
||||
|
||||
private long parseUpdateVersion(BdfList body) throws FormatException {
|
||||
// Client states, update version
|
||||
return body.getLong(1);
|
||||
}
|
||||
|
||||
private List<ClientState> updateStatesFromLocalVersions(
|
||||
List<ClientState> oldStates, List<ClientVersion> newVersions) {
|
||||
Map<ClientVersion, ClientState> oldMap = new HashMap<>();
|
||||
for (ClientState cs : oldStates) oldMap.put(cs.version, cs);
|
||||
List<ClientState> newStates = new ArrayList<>(newVersions.size());
|
||||
for (ClientVersion newVersion : newVersions) {
|
||||
ClientState oldState = oldMap.get(newVersion);
|
||||
boolean active = oldState != null && oldState.active;
|
||||
newStates.add(new ClientState(newVersion, active));
|
||||
}
|
||||
return newStates;
|
||||
}
|
||||
|
||||
private void storeUpdate(Transaction txn, GroupId g,
|
||||
List<ClientState> states, long updateVersion) throws DbException {
|
||||
try {
|
||||
BdfList body = encodeUpdate(states, updateVersion);
|
||||
long now = clock.currentTimeMillis();
|
||||
Message m = clientHelper.createMessage(g, now, body);
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_UPDATE_VERSION, updateVersion);
|
||||
meta.put(MSG_KEY_LOCAL, true);
|
||||
clientHelper.addLocalMessage(txn, m, meta, true);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private BdfList encodeUpdate(List<ClientState> states, long updateVersion) {
|
||||
BdfList encoded = new BdfList();
|
||||
for (ClientState cs : states) encoded.add(encodeClientState(cs));
|
||||
return BdfList.of(encoded, updateVersion);
|
||||
}
|
||||
|
||||
private BdfList encodeClientState(ClientState cs) {
|
||||
return BdfList.of(cs.version.clientId.getString(),
|
||||
cs.version.clientVersion, cs.active);
|
||||
}
|
||||
|
||||
private Map<ClientVersion, Visibility> getVisibilities(
|
||||
List<ClientState> localStates, List<ClientState> remoteStates) {
|
||||
Map<ClientVersion, ClientState> remoteMap = new HashMap<>();
|
||||
for (ClientState remote : remoteStates)
|
||||
remoteMap.put(remote.version, remote);
|
||||
Map<ClientVersion, Visibility> visibilities = new HashMap<>();
|
||||
for (ClientState local : localStates) {
|
||||
ClientState remote = remoteMap.get(local.version);
|
||||
if (remote == null) visibilities.put(local.version, INVISIBLE);
|
||||
else if (remote.active) visibilities.put(local.version, SHARED);
|
||||
else visibilities.put(local.version, VISIBLE);
|
||||
}
|
||||
return visibilities;
|
||||
}
|
||||
|
||||
private void callVisibilityHooks(Transaction txn, Contact c,
|
||||
Map<ClientVersion, Visibility> before,
|
||||
Map<ClientVersion, Visibility> after) throws DbException {
|
||||
Set<ClientVersion> keys = new TreeSet<>();
|
||||
keys.addAll(before.keySet());
|
||||
keys.addAll(after.keySet());
|
||||
for (ClientVersion cv : keys) {
|
||||
Visibility vBefore = before.get(cv), vAfter = after.get(cv);
|
||||
if (vAfter == null) {
|
||||
callVisibilityHook(txn, cv, c, INVISIBLE);
|
||||
} else if (vBefore == null || !vBefore.equals(vAfter)) {
|
||||
callVisibilityHook(txn, cv, c, vAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void callVisibilityHook(Transaction txn, ClientVersion cv,
|
||||
Contact c, Visibility v) throws DbException {
|
||||
ClientVersioningHook hook = hooks.get(cv);
|
||||
if (hook != null) hook.onClientVisibilityChanging(txn, c, v);
|
||||
}
|
||||
|
||||
private void storeFirstUpdate(Transaction txn, GroupId g,
|
||||
List<ClientVersion> versions) throws DbException {
|
||||
List<ClientState> states = new ArrayList<>(versions.size());
|
||||
for (ClientVersion cv : versions)
|
||||
states.add(new ClientState(cv, false));
|
||||
storeUpdate(txn, g, states, 1);
|
||||
}
|
||||
|
||||
private Contact getContact(Transaction txn, GroupId g) throws DbException {
|
||||
try {
|
||||
BdfDictionary meta =
|
||||
clientHelper.getGroupMetadataAsDictionary(txn, g);
|
||||
int id = meta.getLong(GROUP_KEY_CONTACT_ID).intValue();
|
||||
return db.getContact(txn, new ContactId(id));
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ClientState> updateStatesFromRemoteStates(
|
||||
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
||||
Set<ClientVersion> remoteSet = new HashSet<>();
|
||||
for (ClientState remote : remoteStates) remoteSet.add(remote.version);
|
||||
List<ClientState> newLocalStates =
|
||||
new ArrayList<>(oldLocalStates.size());
|
||||
for (ClientState oldState : oldLocalStates) {
|
||||
boolean active = remoteSet.contains(oldState.version);
|
||||
newLocalStates.add(new ClientState(oldState.version, active));
|
||||
}
|
||||
return newLocalStates;
|
||||
}
|
||||
|
||||
private static class Update {
|
||||
|
||||
private final List<ClientState> states;
|
||||
private final long updateVersion;
|
||||
|
||||
private Update(List<ClientState> states, long updateVersion) {
|
||||
this.states = states;
|
||||
this.updateVersion = updateVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LatestUpdate {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final long updateVersion;
|
||||
|
||||
private LatestUpdate(MessageId messageId, long updateVersion) {
|
||||
this.messageId = messageId;
|
||||
this.updateVersion = updateVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LatestUpdates {
|
||||
|
||||
@Nullable
|
||||
private final LatestUpdate local, remote;
|
||||
|
||||
private LatestUpdates(@Nullable LatestUpdate local,
|
||||
@Nullable LatestUpdate remote) {
|
||||
this.local = local;
|
||||
this.remote = remote;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientVersion implements Comparable<ClientVersion> {
|
||||
|
||||
private final ClientId clientId;
|
||||
private final int clientVersion;
|
||||
|
||||
private ClientVersion(ClientId clientId, int clientVersion) {
|
||||
this.clientId = clientId;
|
||||
this.clientVersion = clientVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof ClientVersion) {
|
||||
ClientVersion cv = (ClientVersion) o;
|
||||
return clientId.equals(cv.clientId)
|
||||
&& clientVersion == cv.clientVersion;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (clientId.hashCode() << 16) + clientVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ClientVersion c) {
|
||||
int compare = clientId.compareTo(c.clientId);
|
||||
if (compare != 0) return compare;
|
||||
return clientVersion - c.clientVersion;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientState {
|
||||
|
||||
private final ClientVersion version;
|
||||
private final boolean active;
|
||||
|
||||
private ClientState(ClientVersion version, boolean active) {
|
||||
this.version = version;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
private ClientState(ClientId clientId, int clientVersion,
|
||||
boolean active) {
|
||||
this(new ClientVersion(clientId, clientVersion), active);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof ClientState) {
|
||||
ClientState cs = (ClientState) o;
|
||||
return version.equals(cs.version) && active == cs.active;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return version.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.briarproject.bramble.sync;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.BdfMessageContext;
|
||||
import org.briarproject.bramble.api.client.BdfMessageValidator;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.ClientId.MAX_CLIENT_ID_LENGTH;
|
||||
import static org.briarproject.bramble.sync.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.bramble.sync.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class ClientVersioningValidator extends BdfMessageValidator {
|
||||
|
||||
ClientVersioningValidator(ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||
BdfList body) throws FormatException {
|
||||
// Client states, update version
|
||||
checkSize(body, 2);
|
||||
// Client states
|
||||
BdfList states = body.getList(0);
|
||||
int size = states.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
BdfList clientState = states.getList(i);
|
||||
// Client ID, client version, active
|
||||
checkSize(clientState, 3);
|
||||
String clientId = clientState.getString(0);
|
||||
checkLength(clientId, 1, MAX_CLIENT_ID_LENGTH);
|
||||
int clientVersion = clientState.getLong(1).intValue();
|
||||
if (clientVersion < 0) throw new FormatException();
|
||||
boolean active = clientState.getBoolean(2);
|
||||
}
|
||||
// Update version
|
||||
long updateVersion = body.getLong(1);
|
||||
if (updateVersion < 0) throw new FormatException();
|
||||
// Return the metadata
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_UPDATE_VERSION, updateVersion);
|
||||
meta.put(MSG_KEY_LOCAL, false);
|
||||
return new BdfMessageContext(meta);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
package org.briarproject.bramble.sync;
|
||||
|
||||
import org.briarproject.bramble.PoliteExecutor;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.sync.ClientVersioningManager;
|
||||
import org.briarproject.bramble.api.sync.GroupFactory;
|
||||
import org.briarproject.bramble.api.sync.MessageFactory;
|
||||
import org.briarproject.bramble.api.sync.RecordReaderFactory;
|
||||
@@ -23,12 +27,18 @@ import javax.inject.Singleton;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.ClientVersioningManager.CLIENT_ID;
|
||||
|
||||
@Module
|
||||
public class SyncModule {
|
||||
|
||||
public static class EagerSingletons {
|
||||
@Inject
|
||||
ValidationManager validationManager;
|
||||
@Inject
|
||||
ClientVersioningManager clientVersioningManager;
|
||||
@Inject
|
||||
ClientVersioningValidator clientVersioningValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,4 +100,29 @@ public class SyncModule {
|
||||
return new PoliteExecutor("ValidationExecutor", cryptoExecutor,
|
||||
MAX_CONCURRENT_VALIDATION_TASKS);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ClientVersioningManager provideClientVersioningManager(
|
||||
ClientVersioningManagerImpl clientVersioningManager,
|
||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||
ValidationManager validationManager) {
|
||||
lifecycleManager.registerClient(clientVersioningManager);
|
||||
lifecycleManager.registerService(clientVersioningManager);
|
||||
contactManager.registerContactHook(clientVersioningManager);
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID,
|
||||
clientVersioningManager);
|
||||
return clientVersioningManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ClientVersioningValidator provideClientVersioningValidator(
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock, ValidationManager validationManager) {
|
||||
ClientVersioningValidator validator = new ClientVersioningValidator(
|
||||
clientHelper, metadataEncoder, clock);
|
||||
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,10 +159,9 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
|
||||
deliveryWaiter = new Waiter();
|
||||
|
||||
startLifecycles();
|
||||
|
||||
getDefaultIdentities();
|
||||
addDefaultContacts();
|
||||
listenToEvents();
|
||||
addDefaultContacts();
|
||||
}
|
||||
|
||||
abstract protected void createComponents();
|
||||
@@ -187,7 +186,7 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
|
||||
}
|
||||
|
||||
private void startLifecycles() throws InterruptedException {
|
||||
// Start the lifecycle manager and wait for it to finish
|
||||
// Start the lifecycle manager and wait for it to finish starting
|
||||
lifecycleManager0 = c0.getLifecycleManager();
|
||||
lifecycleManager1 = c1.getLifecycleManager();
|
||||
lifecycleManager2 = c2.getLifecycleManager();
|
||||
@@ -234,7 +233,7 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
|
||||
author2 = identityManager2.getLocalAuthor();
|
||||
}
|
||||
|
||||
protected void addDefaultContacts() throws DbException {
|
||||
protected void addDefaultContacts() throws Exception {
|
||||
contactId1From0 = contactManager0
|
||||
.addContact(author1, author0.getId(), getSecretKey(),
|
||||
clock.currentTimeMillis(), true, true, true);
|
||||
@@ -251,15 +250,25 @@ public abstract class BriarIntegrationTest<C extends BriarIntegrationTestCompone
|
||||
.addContact(author0, author2.getId(), getSecretKey(),
|
||||
clock.currentTimeMillis(), true, true, true);
|
||||
contact0From2 = contactManager2.getContact(contactId0From2);
|
||||
|
||||
// Sync initial client versioning updates
|
||||
sync0To1(1, true);
|
||||
sync0To2(1, true);
|
||||
sync1To0(1, true);
|
||||
sync2To0(1, true);
|
||||
}
|
||||
|
||||
protected void addContacts1And2() throws DbException {
|
||||
protected void addContacts1And2() throws Exception {
|
||||
contactId2From1 = contactManager1
|
||||
.addContact(author2, author1.getId(), getSecretKey(),
|
||||
clock.currentTimeMillis(), true, true, true);
|
||||
contactId1From2 = contactManager2
|
||||
.addContact(author1, author2.getId(), getSecretKey(),
|
||||
clock.currentTimeMillis(), true, true, true);
|
||||
|
||||
// Sync initial client versioning updates
|
||||
sync1To2(1, true);
|
||||
sync2To1(1, true);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
Reference in New Issue
Block a user