| /**************************************************************************** |
| * Copyright (c) 2007 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 |
| *****************************************************************************/ |
| |
| package org.eclipse.ecf.docshare; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InvalidObjectException; |
| import java.io.OutputStream; |
| |
| import org.eclipse.core.filesystem.EFS; |
| import org.eclipse.core.filesystem.IFileStore; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| 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.datashare.events.IChannelDisconnectEvent; |
| import org.eclipse.ecf.docshare.messages.Message; |
| import org.eclipse.ecf.docshare.messages.StartMessage; |
| import org.eclipse.ecf.docshare.messages.StopMessage; |
| import org.eclipse.ecf.docshare.messages.UpdateMessage; |
| import org.eclipse.ecf.internal.docshare.Activator; |
| import org.eclipse.ecf.internal.docshare.Messages; |
| import org.eclipse.jface.dialogs.MessageDialog; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.IEditorDescriptor; |
| import org.eclipse.ui.IEditorInput; |
| import org.eclipse.ui.IEditorRegistry; |
| import org.eclipse.ui.IWorkbench; |
| import org.eclipse.ui.PlatformUI; |
| 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. |
| */ |
| /** |
| * |
| */ |
| public class DocShare extends AbstractShare { |
| |
| /** |
| * The ID of the initiator |
| */ |
| ID initiatorID; |
| /** |
| * The ID of the receiver. |
| */ |
| ID receiverID; |
| /** |
| * Our ID |
| */ |
| ID ourID; |
| /** |
| * Text editor |
| */ |
| 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(); |
| |
| /** |
| * 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 |
| } |
| |
| public void documentChanged(DocumentEvent 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 an remote peer, so we do not |
| //wish to echo this change. |
| return; |
| } |
| // Otherwise end update message |
| sendUpdateMessage(event); |
| } |
| }; |
| |
| /** |
| * 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); |
| } |
| |
| 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); |
| } |
| } |
| |
| /** |
| * 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) { |
| 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 { |
| // Get content from local document |
| final String content = editorPart.getDocumentProvider().getDocument(editorPart.getEditorInput()).get(); |
| // send start message |
| send(toID, new StartMessage(our, fName, toID, content, fileName)); |
| // Set local sharing start (to setup doc listener) |
| localStartShare(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())); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * 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() { |
| if (isSharing()) { |
| // send stop message to other |
| sendStopMessage(); |
| } |
| localStopShare(); |
| } |
| |
| void send(ID toID, Message message) throws Exception { |
| super.sendMessage(toID, message.serialize()); |
| } |
| |
| /* (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 Message message = Message.deserialize(data); |
| Assert.isNotNull(message); |
| if (message instanceof StartMessage) { |
| handleStartMessage((StartMessage) message); |
| } else if (message instanceof UpdateMessage) { |
| handleUpdateMessage((UpdateMessage) 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. |
| */ |
| protected void handleStartMessage(final StartMessage message) { |
| 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); |
| // 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 = (ITextEditor) PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().openEditor(dsei, getEditorIdForFileName(filename)); |
| // Then change our local state |
| localStartShare(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())); |
| } |
| } |
| }); |
| } |
| |
| 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 message the UpdateMessage received. |
| */ |
| protected void handleUpdateMessage(final UpdateMessage message) { |
| final int offset = message.getOffset(); |
| Assert.isTrue(offset > -1); |
| final int length = message.getLength(); |
| Assert.isTrue(length > -1); |
| final String text = message.getText(); |
| Assert.isTrue(text != null); |
| synchronized (stateLock) { |
| // If we're waiting on user to start then change the startContent directly |
| if (startContent != null) { |
| modifyStartContent(offset, length, text); |
| // And we're done |
| return; |
| } |
| } |
| // Else replace in document directly |
| Display.getDefault().asyncExec(new Runnable() { |
| public void run() { |
| try { |
| final IDocument document = getDocumentFromEditor(); |
| if (document != null) { |
| // We setup editor to not take input while we are changing document |
| setEditorToRefuseInput(); |
| document.replace(offset, length, text); |
| } |
| } 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(); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * @param message |
| */ |
| protected void handleStopMessage(StopMessage message) { |
| if (isSharing()) { |
| localStopShare(); |
| } |
| } |
| |
| void setEditorToRefuseInput() { |
| setEditorEditable(false); |
| Activator.getDefault().setListenerActive(false); |
| } |
| |
| void setEditorToAcceptInput() { |
| setEditorEditable(true); |
| Activator.getDefault().setListenerActive(true); |
| } |
| |
| 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) { |
| super.handleDisconnectEvent(cde); |
| localStopShare(); |
| } |
| |
| 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) { |
| 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) { |
| if (editor == null) |
| return null; |
| final IDocumentProvider documentProvider = editor.getDocumentProvider(); |
| if (documentProvider == null) |
| return null; |
| return documentProvider.getDocument(editor.getEditorInput()); |
| } |
| } |
| |
| void localStartShare(ID our, ID initiator, ID receiver, ITextEditor edt) { |
| synchronized (stateLock) { |
| localStopShare(); |
| this.ourID = our; |
| this.initiatorID = initiator; |
| this.receiverID = receiver; |
| this.editor = edt; |
| final IDocument doc = getDocumentFromEditor(); |
| if (doc != null) |
| doc.addDocumentListener(documentListener); |
| } |
| } |
| |
| void localStopShare() { |
| synchronized (stateLock) { |
| this.ourID = null; |
| this.initiatorID = null; |
| this.receiverID = null; |
| this.startContent = null; |
| final IDocument doc = getDocumentFromEditor(); |
| if (doc != null) |
| doc.removeDocumentListener(documentListener); |
| this.editor = null; |
| } |
| } |
| |
| void sendUpdateMessage(DocumentEvent event) { |
| if (isSharing()) { |
| try { |
| send(getOtherID(), new UpdateMessage(event.getOffset(), event.getLength(), event.getText())); |
| } catch (final Exception e) { |
| logError(Messages.DocShare_EXCEPTION_SEND_MESSAGE, e); |
| } |
| } |
| } |
| |
| void sendStopMessage() { |
| sendStopMessage(getOtherID()); |
| } |
| |
| void sendStopMessage(ID other) { |
| if (isSharing()) { |
| try { |
| send(other, new StopMessage()); |
| } catch (final Exception e) { |
| logError(Messages.DocShare_EXCEPTION_SEND_MESSAGE, e); |
| } |
| } |
| } |
| |
| } |