All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jboss.aerogear.sync.client.ClientSyncEngine Maven / Gradle / Ivy

The newest version!
/**
 * 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); } }