// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.collide.client.collaboration;
import com.google.collide.client.collaboration.FileConcurrencyController.DocOpListener;
import com.google.collide.client.document.DocumentManager;
import com.google.collide.client.document.DocumentMetadata;
import com.google.collide.dto.DocOp;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonIntegerMap;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar.RemoverManager;
import com.google.common.base.Preconditions;
import java.util.List;
/**
* A utility class to register for callbacks when all of the doc ops in a particular scope are
* saved (the server has successfully received and applied them to the document.)
*/
public class DocOpsSavedNotifier {
public abstract static class Callback {
private final DocOpListener docOpListener = new DocOpListener() {
@Override
public void onDocOpAckReceived(int documentId, DocOp serverHistoryDocOp, boolean clean) {
Integer remainingAcks = remainingAcksByDocumentId.get(documentId);
if (remainingAcks == null) {
// We have already reached our ack count for this document ID
return;
}
remainingAcks--;
if (remainingAcks == 0) {
remainingAcksByDocumentId.erase(documentId);
tryCallback();
} else {
remainingAcksByDocumentId.put(documentId, remainingAcks);
}
}
@Override
public void onDocOpSent(int documentId, List<DocOp> docOps) {
}
};
private RemoverManager remover;
private JsonIntegerMap<Integer> remainingAcksByDocumentId;
public abstract void onAllDocOpsSaved();
private void initialize(
RemoverManager remover, JsonIntegerMap<Integer> remainingAcksByDocumentId) {
this.remover = remover;
this.remainingAcksByDocumentId = remainingAcksByDocumentId;
}
/**
* Stops listening for the doc ops to be saved.
*/
protected void cancel() {
remover.remove();
remover = null;
remainingAcksByDocumentId = null;
}
private void tryCallback() {
if (!isWaiting()) {
// Only callback after all documents' doc ops have been acked
cancel();
onAllDocOpsSaved();
}
}
boolean isWaiting() {
return remainingAcksByDocumentId != null && !remainingAcksByDocumentId.isEmpty();
}
}
private final DocumentManager documentManager;
private final CollaborationManager collaborationManager;
public DocOpsSavedNotifier(
DocumentManager documentManager, CollaborationManager collaborationManager) {
this.documentManager = documentManager;
this.collaborationManager = collaborationManager;
}
/**
* @see #notifyForFiles(Callback, String...)
*/
public boolean notifyForWorkspace(Callback callback) {
JsonArray<Document> documents = documentManager.getDocuments();
int[] documentIds = new int[documents.size()];
for (int i = 0; i < documentIds.length; i++) {
documentIds[i] = documents.get(i).getId();
}
return notifyForDocuments(callback, documentIds);
}
/**
* @see #notifyForDocuments(Callback, int...)
*/
public boolean notifyForFiles(Callback callback, String... fileEditSessionKeys) {
int[] documentIds = new int[fileEditSessionKeys.length];
for (int i = 0; i < documentIds.length; i++) {
Document document = documentManager.getDocumentByFileEditSessionKey(fileEditSessionKeys[i]);
Preconditions.checkNotNull(document,
"Document for given fileEditSessionKey [" + fileEditSessionKeys[i] + "] does not exist");
documentIds[i] = document.getId();
}
return notifyForDocuments(callback, documentIds);
}
/**
* @return whether we are waiting for unacked or queued doc ops
*/
public boolean notifyForDocuments(Callback callback, int... documentIds) {
RemoverManager remover = new RemoverManager();
JsonIntegerMap<Integer> remainingAcksByDocumentId = JsonCollections.createIntegerMap();
for (int i = 0; i < documentIds.length; i++) {
int documentId = documentIds[i];
if (!DocumentMetadata.isLinkedToFile(documentManager.getDocumentById(documentId))) {
// Ignore unlinked files
continue;
}
DocumentCollaborationController documentCollaborationController =
collaborationManager.getDocumentCollaborationController(documentId);
Preconditions.checkNotNull(documentCollaborationController,
"Could not find collaboration controller document ID [" + documentId + "]");
FileConcurrencyController fileConcurrencyController =
documentCollaborationController.getFileConcurrencyController();
int remainingAcks = computeRemainingAcks(fileConcurrencyController);
if (remainingAcks > 0) {
remainingAcksByDocumentId.put(documentId, remainingAcks);
remover.track(
fileConcurrencyController.getDocOpListenerRegistrar().add(callback.docOpListener));
}
}
callback.initialize(remover, remainingAcksByDocumentId);
// If there aren't any unacked or queued doc ops, this will callback immediately
callback.tryCallback();
return callback.isWaiting();
}
private static int computeRemainingAcks(FileConcurrencyController fileConcurrencyController) {
return fileConcurrencyController.getUnackedClientOpCount()
+ fileConcurrencyController.getQueuedClientOpCount();
}
}