/**************************************************************************** * 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.docshare2; import java.util.HashMap; import java.util.Map; import org.eclipse.core.filebuffers.*; import org.eclipse.core.resources.*; import org.eclipse.core.runtime.*; import org.eclipse.ecf.core.identity.ID; import org.eclipse.ecf.core.util.ECFException; import org.eclipse.ecf.datashare.AbstractShare; import org.eclipse.ecf.datashare.IChannelContainerAdapter; import org.eclipse.ecf.docshare2.messages.*; import org.eclipse.ecf.internal.docshare2.DocShareActivator; import org.eclipse.ecf.sync.IModelChangeMessage; import org.eclipse.ecf.sync.SerializationException; import org.eclipse.ecf.sync.doc.DocumentChangeMessage; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.source.IAnnotationModel; public class DocShare extends AbstractShare { public static final ISynchronizationContext DEFAULT_CONTEXT = new ISynchronizationContext() { public void run(Runnable runnable) { runnable.run(); } }; private ITextFileBufferManager manager = ITextFileBufferManager.DEFAULT; /** * A map of all the documents that are currently shared, keyed based on their workspace-relative paths. */ private Map sharedDocuments; /** * 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); sharedDocuments = new HashMap(); } public synchronized void lock(String[] paths) { // only lock files that needs to be locked for (int i = 0; i < paths.length; i++) { DocumentShare share = (DocumentShare) sharedDocuments.get(paths[i]); if (share != null) { share.lock(); } } } public synchronized void unlock(String[] paths) { for (int i = 0; i < paths.length; i++) { DocumentShare share = (DocumentShare) sharedDocuments.get(paths[i]); if (share != null) { // revert the content, we lock to change the underlying file, now that we're done, we should revert so that the document matches the underlying the file revert(paths[i], manager.getTextFileBuffer(new Path(paths[i]), LocationKind.IFILE)); share.unlock(); } } // for (Iterator it = sharedDocuments.entrySet().iterator(); it.hasNext();) { // Map.Entry entry = (Map.Entry) it.next(); // String path = (String) entry.getKey(); // DocumentShare share = (DocumentShare) entry.getValue(); // share.isLocallyActive(); // revert(path, manager.getTextFileBuffer(new Path(path), LocationKind.IFILE)); // share.isLocallyActive(); // share.unlock(); // } } public void startSharing(ID localID, ID targetID, String path, IAnnotationModel annotationModel) throws CoreException, ECFException { DocumentShare share = (DocumentShare) sharedDocuments.get(path); IDocument document = connect(new Path(path), LocationKind.IFILE); if (share == null) { share = new DocumentShare(getChannel(), targetID, path, document, annotationModel); sharedDocuments.put(path, share); } else { // we're starting to share a file the remote peer already had up, hook up our own listeners share.connect(annotationModel); share.addDocumentListener(); } // starting to share this file, locally active share.setLocallyActive(true); sendStartMessage(localID, targetID, document.get(), path); } private void sendStartMessage(ID localID, ID targetID, String content, String path) throws ECFException { StartMessage message = new StartMessage(localID, content, path); sendMessage(targetID, message.serialize()); } public void sendSelection(ID targetID, String path, int offset, int length) throws ECFException { DocumentShare share = (DocumentShare) sharedDocuments.get(path); // only send selection messages if the other side is active, no point otherwise as the selection isn't shown anyway if (share.isRemotelyActive()) { sendMessage(targetID, new SelectionMessage(path, offset, length).serialize()); } } /** * Stops sharing the document and the specified path and notify the target of this. * @param targetID the target to notify that the document at the path is no longer being shared * @param path the path to the document, must not be <code>null</code> * @throws ECFException */ public void stopSharing(ID targetID, String path) throws ECFException { DocumentShare share = (DocumentShare) sharedDocuments.get(path); if (share.isRemotelyActive()) { share.setLocallyActive(false); } else { sharedDocuments.remove(path); share.removeDocumentListener(); share.disconnect(); disconnect(path); } sendStopMessage(targetID, path); } private void sendStopMessage(ID targetID, String path) throws ECFException { sendMessage(targetID, new StopMessage(path).serialize()); } protected void handleMessage(ID fromContainerID, byte[] data) { try { IModelChangeMessage message = Message.deserialize(data); Assert.isNotNull(message); if (message instanceof SelectionMessage) { handleSelectionMessage((SelectionMessage) message); } else if (message instanceof FileSystemDocumentChangeMessage) { handleFileSystemDocumentChangeMessage((FileSystemDocumentChangeMessage) message); } else if (message instanceof StartMessage) { handleStartMessage((StartMessage) message); } else if (message instanceof StopMessage) { handleStopMessage((StopMessage) message); } } catch (SerializationException e) { DocShareActivator.log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, "Could not deserialize message from " + fromContainerID, e)); //$NON-NLS-1$ } catch (CoreException e) { DocShareActivator.log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, "Could not connect to file buffer", e)); //$NON-NLS-1$ } catch (RuntimeException e) { DocShareActivator.log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, "Runtime exception has occurred while handling message from " + fromContainerID, e)); //$NON-NLS-1$ } } /** * Reverts the content of the file buffer back to what's on the underlying file. * @param path the path of the file * @param buffer the buffer to revert */ private void revert(String path, final IFileBuffer buffer) { // revert within a synchronization context, this is important because an editor may be open on the buffer, in that case, the revert must be done on a UI thread getSynchronizationContext(path).run(new Runnable() { public void run() { try { buffer.revert(null); } catch (CoreException e) { DocShareActivator.log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, "Could not connect to revert buffer for " + buffer.getLocation(), e)); //$NON-NLS-1$ } } }); } protected ISynchronizationContext getSynchronizationContext(String path) { return DEFAULT_CONTEXT; } private void handleFileSystemDocumentChangeMessage(FileSystemDocumentChangeMessage message) { String path = message.getPath(); DocumentShare share = (DocumentShare) sharedDocuments.get(path); if (share != null) { try { documentAboutToBeChanged(path); share.handleUpdateMessage(getSynchronizationContext(path), (DocumentChangeMessage) message.getMessage()); } finally { documentChanged(path); } } } /** * Method that will be called prior to a document being modified by the changes of a remote peer. This is used for performing any preparation work that needs to be invoked prior to the document being modified. * * @param path the path of the document that will be modified */ protected void documentAboutToBeChanged(String path) { // subclasses to override } /** * Method that will be called after a document has been modified by a remote peer's changes. Note that the document may not actually have changed. * * @param path the path of the document that has been modified */ protected void documentChanged(String path) { // subclasses to override } protected void handleStartMessage(StartMessage message) throws CoreException { String sentPath = message.getPath(); DocumentShare share = (DocumentShare) sharedDocuments.get(sentPath); if (share == null) { IPath path = new Path(message.getPath()); IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); IFile file = workspaceRoot.getFile(path); // FIXME: if the file doesn't exist shouldn't we be creating it? LocationKind kind = file.exists() ? LocationKind.IFILE : LocationKind.LOCATION; IDocument document = connect(path, kind); share = new DocumentShare(getChannel(), message.getPeerID(), message.getPath(), document); sharedDocuments.put(message.getPath(), share); } share.setRemotelyActive(true); } private void handleSelectionMessage(final SelectionMessage message) { final DocumentShare share = (DocumentShare) sharedDocuments.get(message.getPath()); if (share != null) { getSynchronizationContext(message.getPath()).run(new Runnable() { public void run() { share.handleSelectionMessage(message); } }); } } protected void handleStopMessage(StopMessage message) { String path = message.getPath(); DocumentShare share = (DocumentShare) sharedDocuments.get(path); if (share != null) { // revert the content, this ensures that unsaved changes are discarded on the other end // TODO: does this make sense? revert(path, manager.getTextFileBuffer(new Path(path), LocationKind.IFILE)); if (share.isLocallyActive()) { // if it's still active locally, just note that it's not remotely active share.setRemotelyActive(false); } else { // not active anywhere, remove it completely sharedDocuments.remove(path); // perform clean-up share.removeDocumentListener(); share.disconnect(); disconnect(path); } } } private IDocument connect(IPath path, LocationKind kind) throws CoreException { manager.connect(path, kind, null); return manager.getTextFileBuffer(path, LocationKind.IFILE).getDocument(); } private void disconnect(String path) { try { manager.disconnect(new Path(path), LocationKind.IFILE, null); } catch (CoreException e) { DocShareActivator.log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, "Could not disconnect file buffer for path: " + path, e)); //$NON-NLS-1$ } } }