| /**************************************************************************** |
| * 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()); |
| } |
| } |