/**************************************************************************** * Copyright (c) 2007, 2009 Composent, Inc. 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: * Composent, Inc. - initial API and implementation * Mustafa K. Isik - conflict resolution via operational transformations * Marcelo Mayworm - Adding sync API dependence * IBM Corporation - support for certain non-text editors *****************************************************************************/ package org.eclipse.ecf.docshare; import org.eclipse.ecf.internal.docshare.Messages; import java.io.*; import java.util.Collections; import java.util.Map; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.runtime.*; import org.eclipse.core.runtime.Assert; import org.eclipse.ecf.core.IContainer; import org.eclipse.ecf.core.identity.ID; import org.eclipse.ecf.core.identity.IDCreateException; import org.eclipse.ecf.core.util.ECFException; import org.eclipse.ecf.core.util.Trace; import org.eclipse.ecf.datashare.AbstractShare; import org.eclipse.ecf.datashare.IChannelContainerAdapter; import org.eclipse.ecf.datashare.events.IChannelDisconnectEvent; import org.eclipse.ecf.docshare.messages.*; import org.eclipse.ecf.internal.docshare.*; import org.eclipse.ecf.presence.IPresenceContainerAdapter; import org.eclipse.ecf.presence.roster.*; import org.eclipse.ecf.sync.*; import org.eclipse.ecf.sync.doc.DocumentChangeMessage; import org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategyFactory; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.*; import org.eclipse.jface.text.source.*; import org.eclipse.jface.viewers.*; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.*; import org.eclipse.ui.*; import org.eclipse.ui.editors.text.EditorsUI; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; /** * Represents a document sharing session between two participants. * @since 2.1 */ public class DocShare extends AbstractShare { public static class SelectionReceiver { private static final String SELECTION_ANNOTATION_ID = "org.eclipse.ecf.docshare.annotations.RemoteSelection"; //$NON-NLS-1$ private static final String CURSOR_ANNOTATION_ID = "org.eclipse.ecf.docshare.annotations.RemoteCursor"; //$NON-NLS-1$ /** * Annotation model of current document */ private IAnnotationModel annotationModel; /** * Object to use as lock for changing in annotation model, * <code>null</code> if no model is provided. */ private Object annotationModelLock; /** * Annotation for remote selection in annotationModel */ private Annotation currentAnnotation; public SelectionReceiver(ITextEditor editor) { if (editor == null) { return; } IDocumentProvider documentProvider = editor.getDocumentProvider(); if (documentProvider != null) { this.annotationModel = documentProvider.getAnnotationModel(editor.getEditorInput()); if (this.annotationModel != null) { if (this.annotationModel instanceof ISynchronizable) { this.annotationModelLock = ((ISynchronizable) this.annotationModel).getLockObject(); } if (this.annotationModelLock == null) { this.annotationModelLock = this; } } } } public void handleMessage(final SelectionMessage remoteMsg) { if (this.annotationModelLock == null) { return; } final Position newPosition = new Position(remoteMsg.getOffset(), remoteMsg.getLength()); final Annotation newAnnotation = new Annotation(newPosition.getLength() > 0 ? SELECTION_ANNOTATION_ID : CURSOR_ANNOTATION_ID, false, Messages.DocShare_RemoteSelection); synchronized (this.annotationModelLock) { if (this.annotationModel != null) { // initial selection, create new if (this.currentAnnotation == null) { this.currentAnnotation = newAnnotation; this.annotationModel.addAnnotation(newAnnotation, newPosition); return; } // selection not changed, skip if (this.currentAnnotation.getType() == newAnnotation.getType()) { Position oldPosition = this.annotationModel.getPosition(this.currentAnnotation); if (oldPosition == null || newPosition.equals(oldPosition)) { return; } } // selection changed, replace annotation if (this.annotationModel instanceof IAnnotationModelExtension) { Annotation[] oldAnnotations = new Annotation[] {this.currentAnnotation}; this.currentAnnotation = newAnnotation; Map newAnnotations = Collections.singletonMap(newAnnotation, newPosition); ((IAnnotationModelExtension) this.annotationModel).replaceAnnotations(oldAnnotations, newAnnotations); } else { this.annotationModel.removeAnnotation(this.currentAnnotation); this.annotationModel.addAnnotation(newAnnotation, newPosition); } } } } public void dispose() { if (this.annotationModelLock == null) { return; } synchronized (this.annotationModelLock) { if (this.annotationModel != null) { if (this.currentAnnotation != null) { this.annotationModel.removeAnnotation(this.currentAnnotation); this.currentAnnotation = null; } this.annotationModel = null; } } } } /** * The ID of the initiator */ private ID initiatorID; /** * The ID of the receiver. */ private ID receiverID; /** * Our ID */ private ID ourID; /** * Text editor */ private ITextEditor editor; /** * Content that we have received via start message, before user has * responded to question about whether or not to display in editor. Should * be null at all other times. */ String startContent = null; /** * Object to use as lock for changing connected state of this docshare * instance */ Object stateLock = new Object(); /** * Strategy for maintaining consistency among session participants' * documents. */ IModelSynchronizationStrategy syncStrategy; /** * Factory to returns the possible strategies */ IDocumentSynchronizationStrategyFactory factory; /** * Handler for SelectionMessage (painting remote selection) */ SelectionReceiver selectionReceiver; /** * Create a document sharing session instance. * * @param adapter * the {@link IChannelContainerAdapter} to use to create this * document sharing session. * @throws ECFException * if the channel cannot be created. */ public DocShare(IChannelContainerAdapter adapter) throws ECFException { super(adapter); factory = Activator.getDefault().getColaSynchronizationStrategyFactory(); } IPartListener partListener = new IPartListener() { public void partActivated(IWorkbenchPart part) { // nothing to do } public void partBroughtToTop(IWorkbenchPart part) { // nothing to do } public void partClosed(IWorkbenchPart part) { ITextEditor textEditor = getTextEditor(); if (textEditor != null && part.equals(textEditor.getSite().getPart())) { stopShare(); } } public void partDeactivated(IWorkbenchPart part) { // nothing to do } public void partOpened(IWorkbenchPart part) { // nothing to do } }; IRosterManager rosterManager; IRosterListener rosterListener = new IRosterListener() { public void handleRosterEntryAdd(IRosterEntry entry) { // nothing to do } public void handleRosterEntryRemove(IRosterEntry entry) { // nothing to do } public void handleRosterUpdate(IRoster roster, IRosterItem changedValue) { if (changedValue instanceof IRosterEntry) { ID changedID = ((IRosterEntry) changedValue).getUser().getID(); ID oID = null; ID otherID = null; Shell shell = null; synchronized (stateLock) { oID = getOurID(); otherID = getOtherID(); IWorkbenchPartSite wps = getTextEditor().getSite(); shell = wps.getShell(); } if (oID != null && changedID.equals(oID)) { localStopShare(); showStopShareMessage(shell, Messages.DocShare_STOP_SHARED_EDITOR_US); } else if (otherID != null && changedID.equals(otherID)) { localStopShare(); showStopShareMessage(shell, Messages.DocShare_STOP_SHARED_EDITOR_REMOTE); } } } }; /** * The document listener is the listener for changes to the *local* copy of * the IDocument. This listener is responsible for sending document update * messages when notified. */ IDocumentListener documentListener = new IDocumentListener() { public void documentAboutToBeChanged(DocumentEvent event) { // nothing to do } // handling of LOCAL OPERATION application public void documentChanged(DocumentEvent event) { // If the channel is gone, then no reason to handle this. if (getChannel() == null || !Activator.getDefault().isListenerActive() || !isSharing()) { return; } // If the listener is not active, ignore input if (!Activator.getDefault().isListenerActive()) { // The local editor is being updated by a remote peer, so we do // not // wish to echo this change. return; } Trace.trace(Activator.PLUGIN_ID, NLS.bind("{0}.documentChanged[{1}]", DocShare.this, event)); //$NON-NLS-1$ // SYNC API. Here is entry point usage of sync API. When a local document is changed by an editor, // this method will be called and the following code executed. This code registers a DocumentChange // with the local syncStrategy instance via syncStrategy.registerLocalChange(IModelChange). // Model change messages returned from the registerLocalChange call are then sent (via ECF datashare channel) // to remote participant. IModelChangeMessage changeMessages[] = syncStrategy.registerLocalChange(new DocumentChangeMessage(event.getOffset(), event.getLength(), event.getText())); for (int i = 0; i < changeMessages.length; i++) { try { sendMessage(getOtherID(), changeMessages[i].serialize()); } catch (final Exception e) { logError(Messages.DocShare_EXCEPTION_SEND_MESSAGE, e); } } } }; ISelectionChangedListener selectionListener = new ISelectionChangedListener() { public void selectionChanged(final SelectionChangedEvent event) { // If the channel is gone, then no reason to handle this. if (getChannel() == null || !Activator.getDefault().isListenerActive()) { return; } // If the listener is not active, ignore input if (!Activator.getDefault().isListenerActive()) { // The local editor is being updated by a remote peer, so we do // not // wish to echo this change. return; } Trace.trace(Activator.PLUGIN_ID, NLS.bind("{0}.selectionChanged[{1}]", DocShare.this, event)); //$NON-NLS-1$ if (!(event.getSelection() instanceof ITextSelection)) { return; } final ITextSelection textSelection = (ITextSelection) event.getSelection(); final SelectionMessage msg = new SelectionMessage(textSelection.getOffset(), textSelection.getLength()); try { sendMessage(getOtherID(), msg.serialize()); } catch (final Exception e) { logError(Messages.DocShare_EXCEPTION_SEND_MESSAGE, e); } } }; /** * Start sharing an editor's contents between two participants. This will * send a request to start sharing with the target identified by the * <code>toID</code> parameter. The remote receiver will be displayed a * message dialog, and given the option to start editor sharing, or not. * * @param our * the ID associated with the initiator. Must not be * <code>null</code>. * @param fromName * a name to present to the receiver. If * <code>null, our.getName() will be used. * @param toID the ID of the intended receiver. Must not be <code>null</code>. * @param fileName the file name of the file to be shared (with suffix type extension). Must not be <code>null</code>. * @param editorPart the text editor currently showing the contents of this editor. Must not be <code>null</code>. */ public void startShare(final ID our, final String fromName, final ID toID, final String fileName, final ITextEditor editorPart) { Trace.entering(Activator.PLUGIN_ID, DocshareDebugOptions.METHODS_ENTERING, DocShare.class, "startShare", new Object[] {our, fromName, toID, fileName, editorPart}); //$NON-NLS-1$ Assert.isNotNull(our); final String fName = (fromName == null) ? our.getName() : fromName; Assert.isNotNull(toID); Assert.isNotNull(fName); Assert.isNotNull(editorPart); Display.getDefault().syncExec(new Runnable() { public void run() { try { // SYNC API. Create the synchronization strategy instance syncStrategy = createSynchronizationStrategy(true); Assert.isNotNull(syncStrategy); // Get content from local document final String content = editorPart.getDocumentProvider().getDocument(editorPart.getEditorInput()).get(); // Send start message with current content sendMessage(toID, new StartMessage(our, fName, toID, content, fileName).serialize()); // Set local sharing start (to setup doc listener) localStartShare(getLocalRosterManager(), our, our, toID, editorPart); } catch (final Exception e) { logError(Messages.DocShare_ERROR_STARTING_EDITOR_TITLE, e); showErrorToUser(Messages.DocShare_ERROR_STARTING_EDITOR_TITLE, NLS.bind(Messages.DocShare_ERROR_STARTING_EDITOR_MESSAGE, e.getLocalizedMessage())); } } }); Trace.exiting(Activator.PLUGIN_ID, DocshareDebugOptions.METHODS_ENTERING, DocShare.class, "startShare"); //$NON-NLS-1$ } /** * Stop editor sharing. Message only sent if we are currently engaged in an * editor sharing session ({@link #isSharing()} returns <code>true</code>. */ public void stopShare() { Trace.entering(Activator.PLUGIN_ID, DocshareDebugOptions.METHODS_ENTERING, this.getClass(), "stopShare"); //$NON-NLS-1$ if (isSharing()) { // send stop message to other sendStopMessage(); syncStrategy = null; } localStopShare(); Trace.exiting(Activator.PLUGIN_ID, DocshareDebugOptions.METHODS_EXITING, this.getClass(), "stopShare"); //$NON-NLS-1$ } /* * (non-Javadoc) * * @see org.eclipse.ecf.datashare.AbstractShare#handleMessage(org.eclipse.ecf.core.identity.ID, * byte[]) */ protected void handleMessage(ID fromContainerID, byte[] data) { try { final IModelChangeMessage message = Message.deserialize(data); Assert.isNotNull(message); if (message instanceof DocumentChangeMessage) { handleUpdateMessage((DocumentChangeMessage) message); } else if (message instanceof SelectionMessage) { SelectionReceiver receiver = selectionReceiver; if (receiver != null) { receiver.handleMessage((SelectionMessage) message); } } else if (message instanceof StartMessage) { handleStartMessage((StartMessage) message); } else if (message instanceof StopMessage) { handleStopMessage((StopMessage) message); } else { throw new InvalidObjectException(NLS.bind(Messages.DocShare_EXCEPTION_INVALID_MESSAGE, message.getClass().getName())); } } catch (final Exception e) { logError(Messages.DocShare_EXCEPTION_HANDLE_MESSAGE, e); } } /** * This method called by the {@link #handleMessage(ID, byte[])} method if * the type of the message received is a start message (sent by remote party * via {@link #startShare(ID, String, ID, String, ITextEditor)}. * * @param message * the UpdateMessage received. * @throws IDCreateException */ protected void handleStartMessage(final StartMessage message) throws IDCreateException { final ID senderID = message.getSenderID(); Assert.isNotNull(senderID); final String senderUsername = message.getSenderUsername(); Assert.isNotNull(senderUsername); final ID our = message.getReceiverID(); Assert.isNotNull(our); final String filename = message.getFilename(); Assert.isNotNull(filename); final String documentContent = message.getDocumentContent(); Assert.isNotNull(documentContent); //SYNC API. Create an instance of the synchronization strategy on the receiver syncStrategy = createSynchronizationStrategy(false); Assert.isNotNull(syncStrategy); // First synchronize on any state changes by getting stateLock synchronized (stateLock) { // If we are already sharing, or have non-null start content if (isSharing() || startContent != null) { sendStopMessage(senderID); // And we're done return; } // Otherwise set start content to the message-provided // documentContent startContent = documentContent; } // Then open UI and show text editor if appropriate Display.getDefault().asyncExec(new Runnable() { public void run() { try { // First, ask user if they want to receive the doc if (openReceiverDialog(senderID, senderUsername, filename)) { // If so, then we create a new DocShareEditorInput final DocShareEditorInput dsei = new DocShareEditorInput(getTempFileStore(senderUsername, filename, startContent), senderUsername, filename); // Then open up text editor final ITextEditor ep; IEditorPart editorPart = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().openEditor(dsei, getEditorIdForFileName(filename)); // if it's not a text editor, offer it a chance to give us one if (editorPart != null && !(editorPart instanceof ITextEditor)) ep = (ITextEditor) editorPart.getAdapter(ITextEditor.class); else ep = (ITextEditor) editorPart; // Then change our local state localStartShare(getLocalRosterManager(), our, senderID, our, ep); } else { // Send stop message to initiator sendStopMessage(); // Then we stop the local share localStopShare(); } } catch (final Exception e) { logError(Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_TITLE, e); showErrorToUser(Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_TITLE, NLS.bind(Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_MESSAGE, e.getLocalizedMessage())); } } }); } IRosterManager getLocalRosterManager() { IContainer container = (IContainer) this.adapter.getAdapter(IContainer.class); if (container != null) { IPresenceContainerAdapter presenceContainerAdapter = (IPresenceContainerAdapter) container.getAdapter(IPresenceContainerAdapter.class); if (presenceContainerAdapter != null) { return presenceContainerAdapter.getRosterManager(); } } return null; } void modifyStartContent(int offset, int length, String text) { final StringBuffer result = new StringBuffer(startContent.substring(0, offset)); result.append(text); result.append(startContent.substring(offset + length)); startContent = result.toString(); } /** * This method called by the {@link #handleMessage(ID, byte[])} method if * the type of the message received is an update message. * * @param documentChangeMessage * the UpdateMessage received. */ protected void handleUpdateMessage(final DocumentChangeMessage documentChangeMessage) { synchronized (stateLock) { // If we're waiting on user to start then change the // startContent // directly if (startContent != null) { modifyStartContent(documentChangeMessage.getOffset(), documentChangeMessage.getLengthOfReplacedText(), documentChangeMessage.getText()); // And we're done return; } } // Else replace in document directly Display.getDefault().asyncExec(new Runnable() { public void run() { try { Trace.entering(Activator.PLUGIN_ID, DocshareDebugOptions.METHODS_ENTERING, this.getClass(), "handleUpdateMessage", documentChangeMessage); //$NON-NLS-1$ final IDocument document = getDocumentFromEditor(); if (document != null) { Trace.trace(Activator.PLUGIN_ID, NLS.bind("{0}.handleUpdateMessage calling transformIncomingMessage", DocShare.this)); //$NON-NLS-1$ // SYNC API. Here a document change message has been received from remote via channel, // and is now passed to the syncStrategy for transformation. The returned IModelChange[] // are then applied to the local document (after the synchronization strategy as transformed // them as necessary). IModelChange modelChanges[] = syncStrategy.transformRemoteChange(documentChangeMessage); // Make editor refuse input while we are applying changes setEditorToRefuseInput(); for (int i = 0; i < modelChanges.length; i++) { // Apply each change to a model. Clients may use this method // to apply the change to a model of appropriate type modelChanges[i].applyToModel(document); } } } catch (final Exception e) { logError(Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_TITLE, e); showErrorToUser(Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_TITLE, NLS.bind(Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_MESSAGE, e.getLocalizedMessage())); } finally { // Have editor accept input setEditorToAcceptInput(); Trace.exiting(Activator.PLUGIN_ID, DocshareDebugOptions.METHODS_EXITING, this.getClass(), "handleUpdateMessage"); //$NON-NLS-1$ } } }); } /** * @param message */ protected void handleStopMessage(StopMessage message) { if (isSharing()) { Shell s = editor.getSite().getShell(); localStopShare(); syncStrategy = null; showStopShareMessage(s, Messages.DocShare_REMOTE_USER_STOPPED); } } void setEditorToRefuseInput() { setEditorEditable(false); Activator.getDefault().setListenerActive(false); } void setEditorToAcceptInput() { setEditorEditable(true); Activator.getDefault().setListenerActive(true); } ITextEditor getEditor() { return editor; } IEditorInput getEditorInput() { synchronized (stateLock) { if (editor == null) return null; return editor.getEditorInput(); } } boolean openReceiverDialog(ID fromID, String fromUsername, String fileName) { return MessageDialog.openQuestion(null, Messages.DocShare_EDITOR_SHARE_POPUP_TITLE, NLS.bind(Messages.DocShare_EDITOR_SHARE_POPUP_MESSAGE, fromUsername, fileName)); } protected void handleDisconnectEvent(IChannelDisconnectEvent cde) { boolean weDisconnected = (ourID != null && ourID.equals(cde.getTargetID())); Shell shell = null; if (isSharing()) { shell = editor.getSite().getShell(); } // Stop things and *then* notify user localStopShare(); if (shell != null) { if (weDisconnected) showStopShareMessage(shell, Messages.DocShare_STOP_SHARED_EDITOR_US); else showStopShareMessage(shell, Messages.DocShare_STOP_SHARED_EDITOR_REMOTE); } } /** * @param shell must not be <code>null</code> * @param message message content for message dialog */ void showStopShareMessage(final Shell shell, final String message) { Display display = shell.getDisplay(); display.asyncExec(new Runnable() { public void run() { MessageDialog.openInformation(shell, Messages.DocShare_STOP_SHARED_EDITOR_TITLE, message); } }); } IFileStore getTempFileStore(String fromUsername, String fileName, String content) throws IOException, CoreException { final IFileStore fileStore = EFS.getLocalFileSystem().fromLocalFile(File.createTempFile(fromUsername, fileName)); final OutputStream outs = fileStore.openOutputStream(EFS.OVERWRITE, null); outs.write(content.getBytes()); outs.close(); return fileStore; } /* * (non-Javadoc) * * @see org.eclipse.ecf.datashare.AbstractShare#dispose() */ public synchronized void dispose() { localStopShare(); super.dispose(); } void logError(IStatus status) { Activator.getDefault().getLog().log(status); } void showErrorToUser(String title, String message) { MessageDialog.openError(null, title, message); } void logError(String exceptionString, Throwable e) { Trace.catching(Activator.PLUGIN_ID, DocshareDebugOptions.EXCEPTIONS_CATCHING, this.getClass(), exceptionString, e); Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, exceptionString, e)); } StyledText getTextControl() { synchronized (stateLock) { if (editor == null) return null; return (StyledText) editor.getAdapter(Control.class); } } void setEditorEditable(final boolean editable) { final StyledText textControl = getTextControl(); if (textControl != null && !textControl.isDisposed()) { Display.getDefault().syncExec(new Runnable() { public void run() { textControl.setEditable(editable); } }); } } String getEditorIdForFileName(String fileName) { final IWorkbench wb = PlatformUI.getWorkbench(); final IEditorRegistry er = wb.getEditorRegistry(); final IEditorDescriptor desc = er.getDefaultEditor(fileName); if (desc != null) return desc.getId(); return EditorsUI.DEFAULT_TEXT_EDITOR_ID; } IDocument getDocumentFromEditor() { synchronized (stateLock) { ITextEditor textEditor = getTextEditor(); if (textEditor == null) return null; final IDocumentProvider documentProvider = textEditor.getDocumentProvider(); if (documentProvider == null) return null; return documentProvider.getDocument(textEditor.getEditorInput()); } } void localStartShare(final IRosterManager rm, ID our, ID initiator, ID receiver, ITextEditor edt) { synchronized (stateLock) { localStopShare(); this.rosterManager = rm; if (this.rosterManager != null) { this.rosterManager.addRosterListener(rosterListener); } this.ourID = our; this.initiatorID = initiator; this.receiverID = receiver; this.editor = edt; this.editor.getSite().getPage().addPartListener(partListener); final IDocument doc = getDocumentFromEditor(); if (doc != null) doc.addDocumentListener(documentListener); if (this.editor != null) { ISelectionProvider selectionProvider = this.editor.getSelectionProvider(); if (selectionProvider instanceof IPostSelectionProvider) { ((IPostSelectionProvider) selectionProvider).addPostSelectionChangedListener(selectionListener); } selectionReceiver = new SelectionReceiver(this.editor); } } } void localStopShare() { SelectionReceiver oldSelectionReceiver; synchronized (stateLock) { if (rosterManager != null) rosterManager.removeRosterListener(rosterListener); this.rosterManager = null; this.ourID = null; this.initiatorID = null; this.receiverID = null; this.startContent = null; final IDocument doc = getDocumentFromEditor(); if (doc != null) doc.removeDocumentListener(documentListener); if (this.editor != null) { this.editor.getSite().getPage().removePartListener(partListener); ISelectionProvider selectionProvider = this.editor.getSelectionProvider(); if (selectionProvider instanceof IPostSelectionProvider) { ((IPostSelectionProvider) selectionProvider).removePostSelectionChangedListener(selectionListener); } } oldSelectionReceiver = this.selectionReceiver; this.selectionReceiver = null; this.editor = null; } if (oldSelectionReceiver != null) { oldSelectionReceiver.dispose(); } } void sendStopMessage() { sendStopMessage(getOtherID()); } void sendStopMessage(ID other) { if (isSharing()) { try { super.sendMessage(other, new StopMessage().serialize()); } catch (final Exception e) { logError(Messages.DocShare_EXCEPTION_SEND_MESSAGE, e); } } } public String toString() { StringBuffer buf = new StringBuffer("DocShare["); //$NON-NLS-1$ buf.append("ourID=").append(ourID).append(";initiatorID=").append(initiatorID).append(";receiverID=").append(receiverID); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ buf.append(";strategy=").append(syncStrategy).append("]"); //$NON-NLS-1$ //$NON-NLS-2$ return buf.toString(); } IModelSynchronizationStrategy createSynchronizationStrategy(boolean isInitiator) { //Instantiate the service Assert.isNotNull(factory); return factory.createDocumentSynchronizationStrategy(getChannel().getID(), isInitiator); } public ID getInitiatorID() { return initiatorID; } public ID getReceiverID() { return receiverID; } public ID getOurID() { return ourID; } public ITextEditor getTextEditor() { return this.editor; } public boolean isSharing() { synchronized (stateLock) { return (this.editor != null); } } public ID getOtherID() { synchronized (stateLock) { if (isInitiator()) return receiverID; return initiatorID; } } public boolean isInitiator() { synchronized (stateLock) { if (ourID == null || initiatorID == null || receiverID == null) return false; return ourID.equals(initiatorID); } } }