org.jboss.aerogear.sync.server.ServerSyncEngine.java Source code

Java tutorial

Introduction

Here is the source code for org.jboss.aerogear.sync.server.ServerSyncEngine.java

Source

/**
 * 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.server;

import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.aerogear.sync.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The ServerSyncEngine is responsible for driving the main differential synchronization algorithm.
 * <p>
 * During construction the engine gets injected with an instance of {@link ServerSynchronizer}
 * which takes care of diff/patching operations, and an instance of {@link ServerDataStore} 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 ServerInMemoryDataStore<JsonNode, JsonPatchEdit> dataStore = new ServerInMemoryDataStore<JsonNode, JsonPatchEdit>();
 * final ServerSyncEngine<JsonNode, JsonPatchEdit> syncEngine = new ServerSyncEngine<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 ServerSyncEngine<T, S extends Edit<? extends Diff>> {

    private static final Logger logger = LoggerFactory.getLogger(ServerSyncEngine.class);
    private static final int SEEDED_CLIENT_VERSION = -1;
    private static final int SEEDED_SERVER_VERSION = 1;
    private static final ConcurrentHashMap<String, Set<Subscriber<?>>> subscribers = new ConcurrentHashMap<String, Set<Subscriber<?>>>();
    private final ServerSynchronizer<T, S> synchronizer;
    private final ServerDataStore<T, S> dataStore;

    /**
     * Sole constructor.
     *
     * @param synchronizer an instance of {@link ServerSynchronizer} that will take care for the diff/patching
     * @param dataStore an instance of {@link ServerDataStore} to store the document/objects
     */
    public ServerSyncEngine(final ServerSynchronizer<T, S> synchronizer, final ServerDataStore<T, S> dataStore) {
        this.synchronizer = synchronizer;
        this.dataStore = dataStore;
    }

    /**
     * Adds a subscriber for the specified document.
     *
     * A server does not create a new document itself, this would be created by a client
     * and a first revision is added to this synchronization engine by this method call.
     *
     * @param subscriber the subscriber to add
     * @param document   the document that the subscriber subscribes to. Will be added to the underlying
     *                   datastore if it does not already exist in the datastore.
     * @return {@link PatchMessage} for the {@link Document}. Will either be an PatchMessage with an empty
     *                   diff, if this is the initial addition of the document, or if the document already
     *                   exists in the underlying datastore the patch message will contain a diff to bring
     *                   the document up to date.
     */
    public PatchMessage<S> addSubscriber(final Subscriber<?> subscriber, final Document<T> document) {
        final PatchMessage<S> patchMessage = addDocument(document, subscriber.clientId());
        connectSubscriber(subscriber, document.id());
        return patchMessage;
    }

    /**
     * Connects a subscriber to an already existing document.
     *
     * @param subscriber the {@link Subscriber} to add
     * @param documentId the id of the document that the subscriber wants to subscribe.
     */
    public void connectSubscriber(final Subscriber<?> subscriber, final String documentId) {
        final Set<Subscriber<?>> newSub = Collections
                .newSetFromMap(new ConcurrentHashMap<Subscriber<?>, Boolean>());
        newSub.add(subscriber);
        while (true) {
            final Set<Subscriber<?>> currentClients = subscribers.get(documentId);
            if (currentClients == null) {
                final Set<Subscriber<?>> previous = subscribers.putIfAbsent(documentId, newSub);
                if (previous != null) {
                    newSub.addAll(previous);
                    if (subscribers.replace(documentId, previous, newSub)) {
                        break;
                    }
                }
            } else {
                newSub.addAll(currentClients);
                if (subscribers.replace(documentId, currentClients, newSub)) {
                    break;
                }
            }
        }
    }

    /**
     * Removes the specified {@link Subscriber}.
     *
     * @param subscriber the {@link Subscriber} to remove
     * @param documentId the document id that the subscriber subscribes to
     */
    public void removeSubscriber(final Subscriber<?> subscriber, final String documentId) {
        while (true) {
            final Set<Subscriber<?>> currentClients = subscribers.get(documentId);
            if (currentClients == null || currentClients.isEmpty()) {
                break;
            }
            final Set<Subscriber<?>> newClients = Collections
                    .newSetFromMap(new ConcurrentHashMap<Subscriber<?>, Boolean>());
            newClients.addAll(currentClients);
            final boolean removed = newClients.remove(subscriber);
            if (removed) {
                if (subscribers.replace(documentId, currentClients, newClients)) {
                    break;
                }
            }
        }
    }

    /**
     * Returns all the subscribers for the specified document.
     *
     * @param documentId the id of the document for which all subscribers should be returned.
     * @return {@code Set} all the {@link Subscriber}s
     */
    public Set<Subscriber<?>> getSubscribers(final String documentId) {
        return subscribers.get(documentId);
    }

    /**
     * Performs the server side diff which is performed when the server document is modified.
     * The produced {@link Edit} can be sent to the client for patching the client side document.
     *
     * @param documentId the document in question.
     * @param clientId the clientId for whom we should perform a diff and create edits for.
     * @return {@link Edit} The server edits, or updates, that were generated by this diff .
     */
    public S diff(final String documentId, final String clientId) {
        final Document<T> document = getDocument(documentId);
        final S edit = serverDiffs(document, clientId);
        diffPatchShadow(getShadowDocument(documentId, clientId), edit);
        return edit;
    }

    /**
     * Performs the server side patching for a specific client.
     *
     * @param patchMessage the changes made by a client.
     * @return {@link PatchMessage} to allow method chaining
     */
    public PatchMessage<S> patch(final PatchMessage<S> patchMessage) {
        final ShadowDocument<T> patchedShadow = patchShadow(patchMessage);
        updateDocument(patchDocument(patchedShadow));
        saveBackupShadow(patchedShadow);
        return patchMessage;
    }

    /**
     * Performs the server side patching for a specific client and updates
     * all subscribers to the patched document.
     *
     * @param patchMessage the changes made by a client.
     */
    public void notifySubscribers(final PatchMessage<S> patchMessage) {
        final S peek = patchMessage.edits().peek();
        if (peek == null) {
            // edits could be null as a client is allowed to send an patch message
            // that only contains an acknowledgement that it has received a specific
            // version from the server.
            return;
        }
        final String documentId = patchMessage.documentId();
        final Set<Subscriber<?>> subscribers1 = getSubscribers(documentId);
        for (Subscriber<?> subscriber : subscribers1) {
            final PatchMessage<?> patchMessage1 = getPatchMessage(documentId, subscriber.clientId());
            logger.debug("Sending to [" + subscriber.clientId() + "] : " + patchMessage1);
            subscriber.patched(patchMessage1);
        }
    }

    /**
     * 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 synchronizer.patchMessageFromJson(json);
    }

    /**
     * Converts the {@link JsonNode} into a {@link Document} instance.
     *
     * @param json the {@link JsonNode} to convert
     * @return {@link Document} the document representing the contents of the {@link JsonNode} instance.
     */
    public Document<T> documentFromJson(final JsonNode json) {
        return synchronizer.documentFromJson(json);
    }

    /**
     * Returns the {@link PatchMessage} for the specified documentId and clientId.
     *
     * @param documentId the document identifier
     * @param clientId the client identifier
     * @return {@link PatchMessage} for the current document/client combination
     */
    public PatchMessage<S> getPatchMessage(final String documentId, final String clientId) {
        diff(documentId, clientId);
        return synchronizer.createPatchMessage(documentId, clientId, dataStore.getEdits(documentId, clientId));
    }

    private PatchMessage<S> addDocument(final Document<T> document, final String clientId) {
        if (document.content() == null) {
            final Document<T> existingDoc = getDocument(document.id());
            if (existingDoc == null) {
                return synchronizer.createPatchMessage(document.id(), clientId, emptyQueue());
            } else {
                final ShadowDocument<T> shadow = addShadowForClient(document.id(), clientId);
                logger.debug("Document with id [" + document.id() + "] already exists.");
                final S edit = serverDiff(shadow.document(), seededShadowFrom(shadow, document));
                updateDocument(patchDocument(shadow));
                return synchronizer.createPatchMessage(document.id(), clientId, asQueue(edit));
            }
        }
        final boolean newDoc = saveDocument(document);
        final ShadowDocument<T> shadow = addShadowForClient(document.id(), clientId);
        if (newDoc) {
            final S edit = serverDiff(shadow.document(), incrementServerVersion(shadow));
            return synchronizer.createPatchMessage(document.id(), clientId, asQueue(edit));
        } else {
            logger.debug("Document with id [" + document.id() + "] already exists.");
            final S edit = serverDiff(shadow.document(), seededShadowFrom(shadow, document));
            return synchronizer.createPatchMessage(document.id(), clientId, asQueue(edit));
        }
    }

    private ShadowDocument<T> seededShadowFrom(final ShadowDocument<T> shadow, final Document<T> doc) {
        final Document<T> document = doc.content() == null ? getDocument(doc.id()) : doc;
        final ClientDocument<T> clientDoc = newClientDocument(doc.id(), shadow.document().clientId(),
                document.content());
        return new DefaultShadowDocument<T>(SEEDED_SERVER_VERSION, SEEDED_CLIENT_VERSION, clientDoc);
    }

    private void diffPatchShadow(final ShadowDocument<T> shadow, final S edit) {
        saveShadow(synchronizer.patchShadow(edit, shadow));
    }

    private ShadowDocument<T> addShadowForClient(final String documentId, final String clientId) {
        return addShadow(documentId, clientId, 0L);
    }

    private ShadowDocument<T> addShadow(final String documentId, final String clientId, final long clientVersion) {
        final Document<T> document = getDocument(documentId);
        final ClientDocument<T> clientDocument = newClientDocument(documentId, clientId, document.content());
        final ShadowDocument<T> shadowDocument = newShadowDoc(0, clientVersion, clientDocument);
        saveShadow(shadowDocument);
        saveBackupShadow(shadowDocument);
        return shadowDocument;
    }

    private S clientDiffs(final Document<T> document, final ShadowDocument<T> shadow) {
        return clientDiff(document, shadow);
    }

    private S serverDiffs(final Document<T> document, final String clientId) {
        final String documentId = document.id();
        final ShadowDocument<T> shadow = getShadowDocument(documentId, clientId);
        final S newEdit = serverDiff(document, shadow);
        saveEdits(newEdit, documentId, clientId);
        saveShadow(incrementServerVersion(shadow));
        return newEdit;
    }

    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 (droppedServerPacket(edit, shadow)) {
                shadow = restoreBackup(shadow, edit);
                continue;
            }
            if (hasClientUpdate(edit, shadow)) {
                discardEdit(edit, documentId, clientId, iterator);
                continue;
            }
            if (allVersionMatch(edit, shadow)) {
                final ShadowDocument<T> patchedShadow = synchronizer.patchShadow(edit, shadow);
                shadow = saveShadowAndRemoveEdit(incrementClientVersion(patchedShadow), edit);
            }
        }
        return shadow;
    }

    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 (serverVersionMatch(backup, edit)) {
            final ShadowDocument<T> patchedShadow = synchronizer.patchShadow(edit, backup.shadow());
            dataStore.removeEdits(documentId, clientId);
            return saveShadow(incrementClientVersion(patchedShadow));
        } else {
            throw new IllegalStateException(
                    backup + " server version does not match version of " + edit.serverVersion());
        }
    }

    private void discardEdit(final S edit, final String documentId, final String clientId,
            final Iterator<S> iterator) {
        dataStore.removeEdit(edit, documentId, clientId);
        iterator.remove();
    }

    private ShadowDocument<T> saveShadowAndRemoveEdit(final ShadowDocument<T> shadow, final S edit) {
        dataStore.removeEdit(edit, shadow.document().id(), shadow.document().clientId());
        return saveShadow(shadow);
    }

    private boolean serverVersionMatch(final BackupShadowDocument<T> backup, final S edit) {
        return backup.version() == edit.serverVersion();
    }

    private boolean droppedServerPacket(final S edit, final ShadowDocument<T> shadowDocument) {
        return edit.serverVersion() < shadowDocument.serverVersion();
    }

    private boolean hasClientUpdate(final S edit, final ShadowDocument<T> shadowDocument) {
        return edit.clientVersion() < shadowDocument.clientVersion();
    }

    private boolean allVersionMatch(final S edit, final ShadowDocument<T> shadowDocument) {
        return edit.serverVersion() == shadowDocument.serverVersion()
                && edit.clientVersion() == shadowDocument.clientVersion();
    }

    private Document<T> patchDocument(final ShadowDocument<T> shadowDocument) {
        final Document<T> document = getDocument(shadowDocument.document().id());
        final S edit = clientDiffs(document, shadowDocument);
        final Document<T> patched = synchronizer.patchDocument(edit, document);
        saveDocument(patched);
        logger.info("Patched Document [" + patched.id() + "] content: " + patched.content());
        return patched;
    }

    private Document<T> getDocument(final String documentId) {
        return dataStore.getDocument(documentId);
    }

    private ClientDocument<T> newClientDocument(final String documentId, final String clientId, final T content) {
        return new DefaultClientDocument<T>(documentId, clientId, content);
    }

    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 S clientDiff(final Document<T> doc, final ShadowDocument<T> shadow) {
        return synchronizer.clientDiff(doc, shadow);
    }

    private S serverDiff(final Document<T> doc, final ShadowDocument<T> shadow) {
        return synchronizer.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> 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.serverVersion(), newShadow));
    }

    private boolean saveDocument(final Document<T> document) {
        return dataStore.saveDocument(document);
    }

    private void updateDocument(final Document<T> document) {
        dataStore.updateDocument(document);
    }

    private Queue<S> emptyQueue() {
        return new LinkedList<S>();
    }

    private Queue<S> asQueue(final S edit) {
        return new LinkedList<S>(Collections.singleton(edit));
    }

}