Java tutorial
/** * 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. * <p> * 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. * <p> * 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. * <p> * * To construct a server that uses the JSON Patch you would use the following code: * <pre> * {@code * final JsonPatchServerSynchronizer synchronizer = new JsonPatchServerSynchronizer(); * final ClientInMemoryDataStore<JsonNode, JsonPatchEdit> dataStore = new ClientInMemoryDataStore<JsonNode, JsonPatchEdit>(); * final ClientSyncEngine<JsonNode, JsonPatchEdit> syncEngine = new ClientSyncEngine<JsonNode, JsonPatchEdit>(synchronizer, dataStore); * }</pre> * * @param <T> The data type data that this implementation can handle. * @param <S> The type of {@link Edit}s that this implementation can handle. */ public class ClientSyncEngine<T, S extends Edit<? extends Diff>> { private static final ObjectMapper OM = new ObjectMapper(); private final ClientSynchronizer<T, S> clientSynchronizer; private final ClientDataStore<T, S> dataStore; private final PatchObservable<T> patchObservable; public ClientSyncEngine(final ClientSynchronizer<T, S> clientSynchronizer, final ClientDataStore<T, S> dataStore, final PatchObservable<T> 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<T> document) { saveDocument(document); saveBackupShadow(saveShadow(new DefaultShadowDocument<T>(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<S> diff(final ClientDocument<T> document) { final String documentId = document.id(); final String clientId = document.clientId(); final ShadowDocument<T> shadow = getShadowDocument(documentId, clientId); final S edit = serverDiff(document, shadow); saveEdits(edit, documentId, clientId); final ShadowDocument<T> 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<S> patchMessage) { final ShadowDocument<T> 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<S> 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<T> 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<S> createPatchMessage(final String documentId, final String clientId, final Queue<S> edits) { return clientSynchronizer.createPatchMessage(documentId, clientId, edits); } private ShadowDocument<T> diffPatchShadow(final ShadowDocument<T> shadow, final S edit) { return clientSynchronizer.patchShadow(edit, shadow); } public void addPatchListener(final PatchListener<T> patchListener) { patchObservable.addPatchListener(patchListener); } public void removePatchListener(final PatchListener<T> patchListener) { patchObservable.removePatchListener(patchListener); } public void removePatchListeners() { patchObservable.removePatchListeners(); } public int countPatchListeners() { return patchObservable.countPatchListeners(); } private ShadowDocument<T> patchShadow(final PatchMessage<S> patchMessage) { final String documentId = patchMessage.documentId(); final String clientId = patchMessage.clientId(); ShadowDocument<T> shadow = getShadowDocument(documentId, clientId); final Iterator<S> 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<T> 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<T> restoreBackup(final ShadowDocument<T> shadow, final S edit) { final String documentId = shadow.document().id(); final String clientId = shadow.document().clientId(); final BackupShadowDocument<T> backup = getBackupShadowDocument(documentId, clientId); if (clientVersionMatch(edit, backup)) { final ShadowDocument<T> 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<T> backup) { return edit.clientVersion() == backup.version(); } private ShadowDocument<T> saveShadowAndRemoveEdit(final ShadowDocument<T> shadow, final S edit) { dataStore.removeEdit(edit, shadow.document().id(), shadow.document().clientId()); return saveShadow(shadow); } private ShadowDocument<T> saveShadow(final ShadowDocument<T> 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<S> iterator) { dataStore.removeEdit(edit, documentId, clientId); iterator.remove(); } private boolean allVersionsMatch(final S edit, final ShadowDocument<T> shadow) { return edit.serverVersion() == shadow.serverVersion() && edit.clientVersion() == shadow.clientVersion(); } private boolean clientPacketDropped(final S edit, final ShadowDocument<T> shadow) { return edit.clientVersion() < shadow.clientVersion() && !isSeedVersion(edit); } private boolean hasServerVersion(final S edit, final ShadowDocument<T> shadow) { return edit.serverVersion() < shadow.serverVersion(); } private Document<T> patchDocument(final ShadowDocument<T> shadowDocument) { final ClientDocument<T> clientDocument = getClientDocumentForShadow(shadowDocument); final S edit = clientDiff(clientDocument, shadowDocument); final ClientDocument<T> patched = patchDocument(edit, clientDocument); saveDocument(patched); saveBackupShadow(shadowDocument); patchObservable.changed(); patchObservable.notifyPatched(patched); return patched; } private ClientDocument<T> patchDocument(final S edit, final ClientDocument<T> clientDocument) { return clientSynchronizer.patchDocument(edit, clientDocument); } private ClientDocument<T> getClientDocumentForShadow(final ShadowDocument<T> shadow) { return dataStore.getClientDocument(shadow.document().id(), shadow.document().clientId()); } private ShadowDocument<T> getShadowDocument(final String documentId, final String clientId) { return dataStore.getShadowDocument(documentId, clientId); } private BackupShadowDocument<T> getBackupShadowDocument(final String documentId, final String clientId) { return dataStore.getBackupShadowDocument(documentId, clientId); } private PatchMessage<S> getPendingEdits(final String documentId, final String clientId) { return clientSynchronizer.createPatchMessage(documentId, clientId, dataStore.getEdits(documentId, clientId)); } private S clientDiff(final ClientDocument<T> doc, final ShadowDocument<T> shadow) { return clientSynchronizer.clientDiff(shadow, doc); } private S serverDiff(final ClientDocument<T> doc, final ShadowDocument<T> 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<T> incrementClientVersion(final ShadowDocument<T> shadow) { final long clientVersion = shadow.clientVersion() + 1; return newShadowDoc(shadow.serverVersion(), clientVersion, shadow.document()); } private ShadowDocument<T> withClientVersion(final ShadowDocument<T> shadow, final long clientVersion) { return newShadowDoc(shadow.serverVersion(), clientVersion, shadow.document()); } private ShadowDocument<T> saveShadow(final ShadowDocument<T> newShadow) { dataStore.saveShadowDocument(newShadow); return newShadow; } private ShadowDocument<T> newShadowDoc(final long serverVersion, final long clientVersion, final ClientDocument<T> doc) { return new DefaultShadowDocument<T>(serverVersion, clientVersion, doc); } private ShadowDocument<T> incrementServerVersion(final ShadowDocument<T> shadow) { final long serverVersion = shadow.serverVersion() + 1; return newShadowDoc(serverVersion, shadow.clientVersion(), shadow.document()); } private void saveBackupShadow(final ShadowDocument<T> newShadow) { dataStore .saveBackupShadowDocument(new DefaultBackupShadowDocument<T>(newShadow.clientVersion(), newShadow)); } private void saveDocument(final ClientDocument<T> document) { dataStore.saveClientDocument(document); } }