/****************************************************************************
* 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$
}
}
}