blob: fe3656ca8cb96e199285cfae253852d4075bb29f [file] [log] [blame]
/****************************************************************************
* Copyright (c) 2008 Mustafa K. Isik and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Mustafa K. Isik - initial API and implementation
*****************************************************************************/
package org.eclipse.ecf.internal.sync.doc.cola;
import java.util.*;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdapterManager;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.util.Trace;
import org.eclipse.ecf.internal.sync.Activator;
import org.eclipse.ecf.internal.sync.SyncDebugOptions;
import org.eclipse.ecf.sync.*;
import org.eclipse.ecf.sync.doc.DocumentChangeMessage;
import org.eclipse.ecf.sync.doc.IDocumentChange;
import org.eclipse.osgi.util.NLS;
public class ColaSynchronizationStrategy implements
IModelSynchronizationStrategy {
// <ColaDocumentChangeMessage>
private final List unacknowledgedLocalOperations;
private final boolean isInitiator;
private long localOperationsCount;
private long remoteOperationsCount;
// <ID, ColaSynchronizationStrategy>
private static Map sessionStrategies = new HashMap();
private ColaSynchronizationStrategy(boolean isInitiator) {
this.isInitiator = isInitiator;
unacknowledgedLocalOperations = new LinkedList();
localOperationsCount = 0;
remoteOperationsCount = 0;
}
public static ColaSynchronizationStrategy getInstanceFor(ID client,
boolean isInitiator) {
ColaSynchronizationStrategy existingStrategy = (ColaSynchronizationStrategy) sessionStrategies
.get(client);
if (existingStrategy != null
&& existingStrategy.isInitiator == isInitiator)
return existingStrategy;
existingStrategy = new ColaSynchronizationStrategy(isInitiator);
sessionStrategies.put(client, existingStrategy);
return existingStrategy;
}
public static void cleanUpFor(ID client) {
sessionStrategies.remove(client);
}
public static void dispose() {
sessionStrategies.clear();
}
/**
* Handles proper transformation of incoming
* <code>ColaDocumentChangeMessage</code>s. Returned
* <code>DocumentChangeMessage</code>s can be applied directly to the shared
* document. The method implements the concurrency algorithm described in
* <code>http://wiki.eclipse.org/RT_Shared_Editing</code>
*
* @param remoteMsg
* @return List contains <code>DocumentChangeMessage</code>s ready for
* sequential application to document
*/
public List transformIncomingMessage(final DocumentChangeMessage remoteMsg) {
if (!(remoteMsg instanceof ColaDocumentChangeMessage)) {
throw new IllegalArgumentException(
"DocumentChangeMessage is incompatible with Cola SynchronizationStrategy"); //$NON-NLS-1$
}
Trace.entering(Activator.PLUGIN_ID, SyncDebugOptions.METHODS_ENTERING,
this.getClass(), "transformIncomingMessage", remoteMsg); //$NON-NLS-1$
ColaDocumentChangeMessage transformedRemote = (ColaDocumentChangeMessage) remoteMsg;
final List transformedRemotes = new LinkedList();
transformedRemotes.add(transformedRemote);
remoteOperationsCount++;
Trace.trace(Activator.PLUGIN_ID, "unacknowledgedLocalOperations="
+ unacknowledgedLocalOperations);
// this is where the concurrency algorithm is executed
if (!unacknowledgedLocalOperations.isEmpty()) {// Do not remove this. It
// is necessary. The
// following iterator
// does not suffice.
// remove operations from queue that have been implicitly
// acknowledged as received on the remote site by the reception of
// this message
for (final Iterator it = unacknowledgedLocalOperations.iterator(); it
.hasNext();) {
final ColaDocumentChangeMessage unackedLocalOp = (ColaDocumentChangeMessage) it
.next();
if (transformedRemote.getRemoteOperationsCount() > unackedLocalOp
.getLocalOperationsCount()) {
Trace.trace(
Activator.PLUGIN_ID,
NLS.bind(
"transformIncomingMessage.removing {0}", unackedLocalOp)); //$NON-NLS-1$
it.remove();
} else {
// the unackowledgedLocalOperations queue is ordered and
// sorted
// due to sequential insertion of local ops, thus once a
// local op with a higher
// or equal local op count (i.e. remote op count from the
// remote operation's view)
// is reached, we can abandon the check for the remaining
// queue items
Trace.trace(Activator.PLUGIN_ID,
"breaking out of unackedLocalOperations loop"); //$NON-NLS-1$
break;// exits for-loop
}
}
// at this point the queue has been freed of operations that
// don't require to be transformed against
if (!unacknowledgedLocalOperations.isEmpty()) {
ColaDocumentChangeMessage localOp = (ColaDocumentChangeMessage) unacknowledgedLocalOperations
.get(0);
Assert.isTrue(transformedRemote.getRemoteOperationsCount() == localOp
.getLocalOperationsCount());
for (final ListIterator unackOpsListIt = unacknowledgedLocalOperations
.listIterator(); unackOpsListIt.hasNext();) {
for (final ListIterator trafoRemotesIt = transformedRemotes
.listIterator(); trafoRemotesIt.hasNext();) {
// returns new instance
// clarify operation preference, owner/docshare
// initiator
// consistently comes first
localOp = (ColaDocumentChangeMessage) unackOpsListIt
.next();
transformedRemote = (ColaDocumentChangeMessage) trafoRemotesIt
.next();
transformedRemote = transformedRemote.transformAgainst(
localOp, isInitiator);
if (transformedRemote.isSplitUp()) {
// currently this only happens for a remote deletion
// that needs to be transformed against a locally
// applied insertion
// attention: before applying a list of deletions to
// docshare, the indices need to be
// updated/finalized one last time
// since deletions can only be applied sequentially
// and every deletion is going to change the
// underlying document and the
// respective indices!
trafoRemotesIt.remove();
for (final Iterator splitUpIterator = transformedRemote
.getSplitUpRepresentation().iterator(); splitUpIterator
.hasNext();) {
trafoRemotesIt.add(splitUpIterator.next());
}
// according to the ListIterator documentation it
// seems so as if the following line is unnecessary,
// as a call to next() after the last removal and
// additions will return what it would have returned
// anyway
// trafoRemotesIt.next();//TODO not sure about the
// need for this - I want to jump over the two
// inserted ops and reach the end of this iterator
}
// TODO check whether or not this collection shuffling
// does what it is supposed to, i.e. remove current
// localop in unack list and add split up representation
// instead
if (localOp.isSplitUp()) {
// local operation has been split up during
// operational transform --> remove current version
// and add new versions plus jump over those
unackOpsListIt.remove();
for (final Iterator splitUpOpIterator = localOp
.getSplitUpRepresentation().iterator(); splitUpOpIterator
.hasNext();) {
unackOpsListIt.add(splitUpOpIterator.next());
}
// according to the ListIterator documentation it
// seems so as if the following line is unnecessary,
// as a call to next() after the last removal and
// additions will return what it would have returned
// anyway
// unackOpsListIt.next();//TODO check whether or not
// this does jump over both inserted operations that
// replaced the current unack op
}// end split up localop handling
}// transformedRemotes List iteration
}
}
}
Trace.exiting(Activator.PLUGIN_ID, SyncDebugOptions.METHODS_EXITING,
this.getClass(), "transformIncomingMessage", transformedRemote); //$NON-NLS-1$
// TODO find a cleaner and more OO way of cleaning up the list if it
// contains multiple deletions:
if (transformedRemotes.size() > 1) {
final ColaDocumentChangeMessage firstOp = (ColaDocumentChangeMessage) transformedRemotes
.get(0);
if (firstOp.isDeletion()) {
// this means all operations in the return list must also be
// deletions, i.e. modify virtual/optimistic offset for
// sequential application to document
final ListIterator deletionFinalizerIt = transformedRemotes
.listIterator();
ColaDocumentChangeMessage previousDel = (ColaDocumentChangeMessage) deletionFinalizerIt
.next();// jump over first del-op does not need
// modification, we know this is OK because of
// previous size check;
ColaDocumentChangeMessage currentDel;
for (; deletionFinalizerIt.hasNext();) {
currentDel = (ColaDocumentChangeMessage) deletionFinalizerIt
.next();
currentDel.setOffset(currentDel.getOffset()
- previousDel.getLengthOfReplacedText());
previousDel = currentDel;
}
}
}
return transformedRemotes;
}
public String toString() {
final StringBuffer buf = new StringBuffer("ColaSynchronizationStrategy"); //$NON-NLS-1$
return buf.toString();
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategy#registerLocalChange
* (org.eclipse.ecf.sync.doc.IModelChange)
*/
public IModelChangeMessage[] registerLocalChange(IModelChange localChange) {
List results = new ArrayList();
Trace.entering(Activator.PLUGIN_ID, SyncDebugOptions.METHODS_ENTERING,
this.getClass(), "registerLocalChange", localChange); //$NON-NLS-1$
if (localChange instanceof IDocumentChange) {
final IDocumentChange docChange = (IDocumentChange) localChange;
final ColaDocumentChangeMessage colaMsg = new ColaDocumentChangeMessage(
new DocumentChangeMessage(docChange.getOffset(),
docChange.getLengthOfReplacedText(),
docChange.getText()), localOperationsCount,
remoteOperationsCount);
// If not replacement, we simply add to
// unacknowledgedLocalOperations and add message
// to results
if (!colaMsg.isReplacement()) {
unacknowledgedLocalOperations.add(colaMsg);
localOperationsCount++;
results.add(colaMsg);
} else {
// It *is a replacement message, so we add both a delete and an
// insert message
// First create/add a delete message (text set to "")...
ColaDocumentChangeMessage delMsg = new ColaDocumentChangeMessage(
new DocumentChangeMessage(docChange.getOffset(),
docChange.getLengthOfReplacedText(), ""),
localOperationsCount, remoteOperationsCount);
unacknowledgedLocalOperations.add(delMsg);
localOperationsCount++;
results.add(delMsg);
// Then create/add the insert message (length set to 0)
ColaDocumentChangeMessage insMsg = new ColaDocumentChangeMessage(
new DocumentChangeMessage(docChange.getOffset(), 0,
docChange.getText()), localOperationsCount,
remoteOperationsCount);
unacknowledgedLocalOperations.add(insMsg);
localOperationsCount++;
results.add(insMsg);
}
Trace.exiting(Activator.PLUGIN_ID,
SyncDebugOptions.METHODS_EXITING, this.getClass(),
"registerLocalChange", colaMsg); //$NON-NLS-1$
}
return (IModelChangeMessage[]) results
.toArray(new IModelChangeMessage[] {});
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategy#
* toDocumentChangeMessage(byte[])
*/
public IModelChange deserializeRemoteChange(byte[] bytes)
throws SerializationException {
return DocumentChangeMessage.deserialize(bytes);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategy#
* transformRemoteChange(org.eclipse.ecf.sync.doc.IModelChangeMessage)
*/
public IModelChange[] transformRemoteChange(IModelChange remoteChange) {
if (!(remoteChange instanceof DocumentChangeMessage))
return new IDocumentChange[0];
final DocumentChangeMessage m = (DocumentChangeMessage) remoteChange;
final List l = this.transformIncomingMessage(m);
return (IDocumentChange[]) l.toArray(new IDocumentChange[] {});
}
/*
* (non-Javadoc)
*
* @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
*/
public Object getAdapter(Class adapter) {
if (adapter == null)
return null;
IAdapterManager manager = Activator.getDefault().getAdapterManager();
if (manager == null)
return null;
return manager.loadAdapter(this, adapter.getName());
}
}