// 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.IncomingDocOpDemultiplexer.Receiver; import com.google.collide.client.collaboration.cc.GenericOperationChannel.ReceiveOpChannel; import com.google.collide.client.collaboration.cc.RevisionProvider; import com.google.collide.client.util.ClientTimer; import com.google.collide.client.util.logging.Log; import com.google.collide.dto.DocOp; import com.google.collide.dto.DocumentSelection; import com.google.collide.dto.ServerToClientDocOp; import com.google.collide.dto.client.DtoClientImpls.ServerToClientDocOpImpl; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.Pair; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.Reorderer; import com.google.collide.shared.util.Reorderer.ItemSink; import com.google.collide.shared.util.Reorderer.TimeoutCallback; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; /** * Helper to receive messages from the transport and pass it onto the local * concurrency control library. * */ class DocOpReceiver implements ReceiveOpChannel<DocOp> { private ReceiveOpChannel.Listener<DocOp> listener; @VisibleForTesting final Receiver unorderedDocOpReceiver = new Receiver() { @Override public void onDocOpReceived(ServerToClientDocOpImpl message, DocOp docOp) { // We just received this doc op from the wire, pass it to the reorderer docOpReorderer.acceptItem(Pair.of(message, docOp), message.getAppliedCcRevision()); } }; private final ItemSink<Pair<ServerToClientDocOpImpl, DocOp>> orderedDocOpSink = new ItemSink<Pair<ServerToClientDocOpImpl, DocOp>>() { @Override public void onItem(Pair<ServerToClientDocOpImpl, DocOp> docOpPair, int version) { onReceivedOrderedDocOp(docOpPair.first, docOpPair.second, false); } }; private Reorderer<Pair<ServerToClientDocOpImpl, DocOp>> docOpReorderer; private final TimeoutCallback outOfOrderTimeoutCallback; private final int outOfOrderTimeoutMs; private final String fileEditSessionKey; /** Valid only for a partial scope of messageReceiver */ private String currentMessageClientId; /** Valid only for a partial scope of messageReceiver */ private DocumentSelection currentMessageSelection; private final IncomingDocOpDemultiplexer docOpDemux; private boolean isPaused; private final JsonArray<ServerToClientDocOp> queuedOrderedServerToClientDocOps = JsonCollections .createArray(); private RevisionProvider revisionProvider; DocOpReceiver(IncomingDocOpDemultiplexer docOpDemux, String fileEditSessionKey, Reorderer.TimeoutCallback outOfOrderTimeoutCallback, int outOfOrderTimeoutMs) { this.docOpDemux = docOpDemux; this.fileEditSessionKey = fileEditSessionKey; this.outOfOrderTimeoutCallback = outOfOrderTimeoutCallback; this.outOfOrderTimeoutMs = outOfOrderTimeoutMs; } void setRevisionProvider(RevisionProvider revisionProvider) { this.revisionProvider = revisionProvider; } @Override public void connect(int revision, ReceiveOpChannel.Listener<DocOp> listener) { Preconditions.checkState(revisionProvider != null, "Must have set revisionProvider by now"); this.listener = listener; int nextExpectedVersion = revision + 1; this.docOpReorderer = Reorderer.create( nextExpectedVersion, orderedDocOpSink, outOfOrderTimeoutMs, outOfOrderTimeoutCallback, ClientTimer.FACTORY); docOpDemux.setReceiver(fileEditSessionKey, unorderedDocOpReceiver); } @Override public void disconnect() { docOpDemux.setReceiver(fileEditSessionKey, null); } /** * Pauses the processing (calling back to listener) of received doc ops. While paused, any * received doc ops will be stored in a queue which can be retrieved via * {@link #getOrderedQueuedServerToClientDocOps()}. This queue will only contain doc ops from this * point forward. * * <p> * This method can be called multiple times without calling {@link #resume(int)}. */ void pause() { if (isPaused) { return; } isPaused = true; docOpReorderer.setTimeoutEnabled(false); // Clear queue so it will contain only doc ops after this pause queuedOrderedServerToClientDocOps.clear(); } JsonArray<ServerToClientDocOp> getOrderedQueuedServerToClientDocOps() { return queuedOrderedServerToClientDocOps; } /** * Resumes the processing of received doc ops. * * <p> * While this was paused, doc ops were accumulated in the queue * {@link #getOrderedQueuedServerToClientDocOps()}. Those will not be processed * automatically. * * <p> * This method cannot be called when already resumed. */ void resume(int nextExpectedVersion) { Preconditions.checkState(isPaused, "Cannot resume if already resumed"); isPaused = false; docOpReorderer.setTimeoutEnabled(true); docOpReorderer.skipToVersion(nextExpectedVersion); } String getClientId() { return currentMessageClientId; } DocumentSelection getSelection() { return currentMessageSelection; } /** * @param bypassPaused whether to process the doc op immediately even if * {@link #pause()} has been called */ void simulateOrderedDocOpReceived(ServerToClientDocOpImpl message, boolean bypassPaused) { DocOp docOp = message.getDocOp2(); onReceivedOrderedDocOp(message, docOp, bypassPaused); } private void onReceivedOrderedDocOp( ServerToClientDocOpImpl message, DocOp docOp, boolean bypassPaused) { if (isPaused && !bypassPaused) { // Just queue the doc op messages instead queuedOrderedServerToClientDocOps.add(message); return; } if (revisionProvider.revision() >= message.getAppliedCcRevision()) { // Already seen this return; } /* * Later in the stack, we need this valuable information for rendering the * collaborator's cursor. But, since we funnel through the concurrency * control library, this information is lost. The workaround is to stash * the data here. We will fetch this from the callback given by the * concurrency control library. * * TODO: This is really a workaround until I have time to * think about a clean API for sending/receiving positions like the * collaborative selection/cursor stuff requires. Once I do that, I'll * also remove this workaround and make position transformation a * first-class feature inside the forked CC library. */ currentMessageClientId = message.getClientId(); currentMessageSelection = message.getSelection(); try { listener.onMessage(message.getAppliedCcRevision(), message.getClientId(), docOp); } catch (Throwable t) { Log.error(getClass(), "Could not handle received doc op", t); } currentMessageClientId = null; currentMessageSelection = null; } }