
org.jboss.aerogear.sync.client.ClientSyncEngine Maven / Gradle / Ivy
/**
* JBoss, Home of Professional Open Source
* Copyright Red Hat, Inc., and individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.aerogear.sync.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.aerogear.sync.BackupShadowDocument;
import org.jboss.aerogear.sync.ClientDocument;
import org.jboss.aerogear.sync.DefaultBackupShadowDocument;
import org.jboss.aerogear.sync.DefaultShadowDocument;
import org.jboss.aerogear.sync.Diff;
import org.jboss.aerogear.sync.Document;
import org.jboss.aerogear.sync.Edit;
import org.jboss.aerogear.sync.PatchMessage;
import org.jboss.aerogear.sync.ShadowDocument;
import java.util.Iterator;
import java.util.Queue;
/**
* The ClientSyncEngine is responsible for driving client side of the differential synchronization algorithm.
*
* During construction the engine gets injected with an instance of {@link ClientSynchronizer}
* which takes care of diff/patching operations, and an instance of {@link ClientDataStore} for
* storing data.
*
* A synchronizer in AeroGear is a module that serves two purposes which are closely related. One, is to provide
* storage for the data type, and the second is to provide the patching algorithm to be used on that data type.
* The name synchronizer is because they take care of the synchronization part of the Differential Synchronization
* algorithm. For example, one synchronizer might support plain text while another supports JSON Objects as the
* content of documents being stored. But a patching algorithm used for plain text might not be appropriate for JSON
* Objects.
*
*
* To construct a server that uses the JSON Patch you would use the following code:
*
* {@code
* final JsonPatchServerSynchronizer synchronizer = new JsonPatchServerSynchronizer();
* final ClientInMemoryDataStore dataStore = new ClientInMemoryDataStore();
* final ClientSyncEngine syncEngine = new ClientSyncEngine(synchronizer, dataStore);
* }
*
* @param The data type data that this implementation can handle.
* @param The type of {@link Edit}s that this implementation can handle.
*/
public class ClientSyncEngine> {
private static final ObjectMapper OM = new ObjectMapper();
private final ClientSynchronizer clientSynchronizer;
private final ClientDataStore dataStore;
private final PatchObservable patchObservable;
public ClientSyncEngine(final ClientSynchronizer clientSynchronizer,
final ClientDataStore dataStore,
final PatchObservable patchObservable) {
this.clientSynchronizer = clientSynchronizer;
this.dataStore = dataStore;
this.patchObservable = patchObservable;
}
/**
* Adds a new document to this sync engine.
*
* @param document the document to add.
*/
public void addDocument(final ClientDocument document) {
saveDocument(document);
saveBackupShadow(saveShadow(new DefaultShadowDocument(0, 0, document)));
}
/**
* Returns an {@link PatchMessage} which contains a diff against the engine's stored
* shadow document and the passed-in document.
*
* There might be pending edits that represent edits that have not made it to the server
* for some reason (for example packet drop). If a pending edit exits the contents (the diffs)
* of the pending edit will be included in the returned Edits from this method.
*
* The returned {@link PatchMessage} instance is indended to be sent to the server engine
* for processing.
*
* @param document the updated document.
* @return {@link PatchMessage} containing the edits for the changes in the document.
*/
public PatchMessage diff(final ClientDocument document) {
final String documentId = document.id();
final String clientId = document.clientId();
final ShadowDocument shadow = getShadowDocument(documentId, clientId);
final S edit = serverDiff(document, shadow);
saveEdits(edit, documentId, clientId);
final ShadowDocument patchedShadow = diffPatchShadow(shadow, edit);
saveShadow(incrementClientVersion(patchedShadow));
return getPendingEdits(document.id(), document.clientId());
}
/**
* Patches the client side shadow with updates ({@link PatchMessage}) from the server.
*
* When updates happen on the server, the server will create an {@link PatchMessage} instance
* by calling the server engines diff method. This {@link PatchMessage} instance will then be
* sent to the client for processing which is done by this method.
*
* @param patchMessage the updates from the server.
*/
public void patch(final PatchMessage patchMessage) {
final ShadowDocument patchedShadow = patchShadow(patchMessage);
patchDocument(patchedShadow);
saveBackupShadow(patchedShadow);
}
/**
* Creates a {link PatchMessage} by parsing the passed-in json.
*
* @param json the json representation of a {@code PatchMessage}
* @return {@link PatchMessage} the created {code PatchMessage}
*/
public PatchMessage patchMessageFromJson(final String json) {
return clientSynchronizer.patchMessageFromJson(json);
}
/**
* Converts the {@link ClientDocument} into a JSON {@code String} representation.
*
* @param document the {@link ClientDocument} to convert
* @return {@code String} the JSON String representation of the document.
*/
public String documentToJson(final ClientDocument document) {
final ObjectNode objectNode = OM.createObjectNode();
objectNode.put("msgType", "add");
objectNode.put("id", document.id());
objectNode.put("clientId", document.clientId());
clientSynchronizer.addContent(document.content(), objectNode, "content");
return objectNode.toString();
}
/**
* Creates a new {@link PatchMessage} with the with the type of {@link Edit} that this
* synchronizer can handle.
*
* @param documentId the document identifier for the {@code PatchMessage}
* @param clientId the client identifier for the {@code PatchMessage}
* @param edits the {@link Edit}s for the {@code PatchMessage}
* @return {@link PatchMessage} the created {code PatchMessage}
*/
public PatchMessage createPatchMessage(final String documentId, final String clientId, final Queue edits) {
return clientSynchronizer.createPatchMessage(documentId, clientId, edits);
}
private ShadowDocument diffPatchShadow(final ShadowDocument shadow, final S edit) {
return clientSynchronizer.patchShadow(edit, shadow);
}
public void addPatchListener(final PatchListener patchListener) {
patchObservable.addPatchListener(patchListener);
}
public void removePatchListener(final PatchListener patchListener) {
patchObservable.removePatchListener(patchListener);
}
public void removePatchListeners() {
patchObservable.removePatchListeners();
}
public int countPatchListeners() {
return patchObservable.countPatchListeners();
}
private ShadowDocument patchShadow(final PatchMessage patchMessage) {
final String documentId = patchMessage.documentId();
final String clientId = patchMessage.clientId();
ShadowDocument shadow = getShadowDocument(documentId, clientId);
final Iterator iterator = patchMessage.edits().iterator();
while (iterator.hasNext()) {
final S edit = iterator.next();
if (clientPacketDropped(edit, shadow)) {
shadow = restoreBackup(shadow, edit);
continue;
}
if (hasServerVersion(edit, shadow)) {
discardEdit(edit, documentId, clientId, iterator);
continue;
}
if (allVersionsMatch(edit, shadow) || isSeedVersion(edit)) {
final ShadowDocument patchedShadow = clientSynchronizer.patchShadow(edit, shadow);
if (isSeedVersion(edit)) {
shadow = saveShadowAndRemoveEdit(withClientVersion(patchedShadow, 0), edit);
} else {
shadow = saveShadowAndRemoveEdit(incrementServerVersion(patchedShadow), edit);
}
}
}
return shadow;
}
private boolean isSeedVersion(final S edit) {
return edit.clientVersion() == -1;
}
private ShadowDocument restoreBackup(final ShadowDocument shadow,
final S edit) {
final String documentId = shadow.document().id();
final String clientId = shadow.document().clientId();
final BackupShadowDocument backup = getBackupShadowDocument(documentId, clientId);
if (clientVersionMatch(edit, backup)) {
final ShadowDocument patchedShadow = clientSynchronizer.patchShadow(edit, backup.shadow());
dataStore.removeEdits(documentId, clientId);
return saveShadow(incrementServerVersion(patchedShadow), edit);
} else {
throw new IllegalStateException("Backup version [" + backup.version() + "] does not match edit client version [" + edit.clientVersion() + ']');
}
}
private boolean clientVersionMatch(final S edit, final BackupShadowDocument backup) {
return edit.clientVersion() == backup.version();
}
private ShadowDocument saveShadowAndRemoveEdit(final ShadowDocument shadow, final S edit) {
dataStore.removeEdit(edit, shadow.document().id(), shadow.document().clientId());
return saveShadow(shadow);
}
private ShadowDocument saveShadow(final ShadowDocument shadow, final S edit) {
dataStore.removeEdit(edit, shadow.document().id(), shadow.document().clientId());
return saveShadow(shadow);
}
private void discardEdit(final S edit, final String documentId, final String clientId, final Iterator iterator) {
dataStore.removeEdit(edit, documentId, clientId);
iterator.remove();
}
private boolean allVersionsMatch(final S edit, final ShadowDocument shadow) {
return edit.serverVersion() == shadow.serverVersion() && edit.clientVersion() == shadow.clientVersion();
}
private boolean clientPacketDropped(final S edit, final ShadowDocument shadow) {
return edit.clientVersion() < shadow.clientVersion() && !isSeedVersion(edit);
}
private boolean hasServerVersion(final S edit, final ShadowDocument shadow) {
return edit.serverVersion() < shadow.serverVersion();
}
private Document patchDocument(final ShadowDocument shadowDocument) {
final ClientDocument clientDocument = getClientDocumentForShadow(shadowDocument);
final S edit = clientDiff(clientDocument, shadowDocument);
final ClientDocument patched = patchDocument(edit, clientDocument);
saveDocument(patched);
saveBackupShadow(shadowDocument);
patchObservable.changed();
patchObservable.notifyPatched(patched);
return patched;
}
private ClientDocument patchDocument(final S edit, final ClientDocument clientDocument) {
return clientSynchronizer.patchDocument(edit, clientDocument);
}
private ClientDocument getClientDocumentForShadow(final ShadowDocument shadow) {
return dataStore.getClientDocument(shadow.document().id(), shadow.document().clientId());
}
private ShadowDocument getShadowDocument(final String documentId, final String clientId) {
return dataStore.getShadowDocument(documentId, clientId);
}
private BackupShadowDocument getBackupShadowDocument(final String documentId, final String clientId) {
return dataStore.getBackupShadowDocument(documentId, clientId);
}
private PatchMessage getPendingEdits(final String documentId, final String clientId) {
return clientSynchronizer.createPatchMessage(documentId, clientId, dataStore.getEdits(documentId, clientId));
}
private S clientDiff(final ClientDocument doc, final ShadowDocument shadow) {
return clientSynchronizer.clientDiff(shadow, doc);
}
private S serverDiff(final ClientDocument doc, final ShadowDocument shadow) {
return clientSynchronizer.serverDiff(doc, shadow);
}
private void saveEdits(final S edit, final String documentId, final String clientId) {
dataStore.saveEdits(edit, documentId, clientId);
}
private ShadowDocument incrementClientVersion(final ShadowDocument shadow) {
final long clientVersion = shadow.clientVersion() + 1;
return newShadowDoc(shadow.serverVersion(), clientVersion, shadow.document());
}
private ShadowDocument withClientVersion(final ShadowDocument shadow, final long clientVersion) {
return newShadowDoc(shadow.serverVersion(), clientVersion, shadow.document());
}
private ShadowDocument saveShadow(final ShadowDocument newShadow) {
dataStore.saveShadowDocument(newShadow);
return newShadow;
}
private ShadowDocument newShadowDoc(final long serverVersion, final long clientVersion, final ClientDocument doc) {
return new DefaultShadowDocument(serverVersion, clientVersion, doc);
}
private ShadowDocument incrementServerVersion(final ShadowDocument shadow) {
final long serverVersion = shadow.serverVersion() + 1;
return newShadowDoc(serverVersion, shadow.clientVersion(), shadow.document());
}
private void saveBackupShadow(final ShadowDocument newShadow) {
dataStore.saveBackupShadowDocument(new DefaultBackupShadowDocument(newShadow.clientVersion(), newShadow));
}
private void saveDocument(final ClientDocument document) {
dataStore.saveClientDocument(document);
}
}