diff --git a/core/src/saros/activities/DeletionAcknowledgmentActivity.java b/core/src/saros/activities/DeletionAcknowledgmentActivity.java new file mode 100644 index 0000000000..f51fe3137d --- /dev/null +++ b/core/src/saros/activities/DeletionAcknowledgmentActivity.java @@ -0,0 +1,28 @@ +package saros.activities; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import saros.session.User; +import saros.session.internal.DeletionAcknowledgmentDispatcher; + +/** + * Activity for notifying other participants that a file was successfully deleted locally. + * + * @see DeletionAcknowledgmentDispatcher + */ +@XStreamAlias("deletionAcknowledgementActivity") +public class DeletionAcknowledgmentActivity extends AbstractResourceActivity { + + public DeletionAcknowledgmentActivity(User user, SPath resource) { + super(user, resource); + } + + @Override + public void dispatch(IActivityReceiver receiver) { + receiver.receive(this); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + " : " + getSource() + " - " + getPath(); + } +} diff --git a/core/src/saros/activities/FolderDeletedActivity.java b/core/src/saros/activities/FolderDeletedActivity.java index b11a2fb51c..b7f6f6a668 100644 --- a/core/src/saros/activities/FolderDeletedActivity.java +++ b/core/src/saros/activities/FolderDeletedActivity.java @@ -1,9 +1,19 @@ package saros.activities; import com.thoughtworks.xstream.annotations.XStreamAlias; +import saros.concurrent.management.ConcurrentDocumentClient; +import saros.concurrent.management.ConcurrentDocumentServer; import saros.session.User; -/** An activity that represents the deletion of a folder made by a user during a session. */ +/** + * An activity that represents the deletion of a folder made by a user during a session. + * + *
NOTE: Any resource that is contained in the deleted folder should have been processed + * separately before dispatching the folder deletion activity. This is important to allow the other + * session participants to clean up the state of all deleted child resources. Furthermore, the + * explicit handling of deleted child resources is required by the {@link ConcurrentDocumentServer} + * and {@link ConcurrentDocumentClient}. + */ @XStreamAlias("folderDeleted") public class FolderDeletedActivity extends AbstractResourceActivity implements IFileSystemModificationActivity { diff --git a/core/src/saros/activities/IActivityReceiver.java b/core/src/saros/activities/IActivityReceiver.java index dd57bc5dd9..1fa6978ac6 100644 --- a/core/src/saros/activities/IActivityReceiver.java +++ b/core/src/saros/activities/IActivityReceiver.java @@ -31,6 +31,10 @@ default void receive(ChecksumErrorActivity checksumErrorActivity) { /*NOP*/ } + default void receive(DeletionAcknowledgmentActivity deletionAcknowledgmentActivity) { + /*NOP*/ + } + default void receive(EditorActivity editorActivity) { /*NOP*/ } diff --git a/core/src/saros/communication/extensions/ActivitiesExtension.java b/core/src/saros/communication/extensions/ActivitiesExtension.java index 65d7e60bcc..7ca1986451 100644 --- a/core/src/saros/communication/extensions/ActivitiesExtension.java +++ b/core/src/saros/communication/extensions/ActivitiesExtension.java @@ -27,6 +27,7 @@ import saros.activities.ChangeColorActivity; import saros.activities.ChecksumActivity; import saros.activities.ChecksumErrorActivity; +import saros.activities.DeletionAcknowledgmentActivity; import saros.activities.EditorActivity; import saros.activities.FileActivity; import saros.activities.FolderCreatedActivity; @@ -143,6 +144,7 @@ private Provider() { ChangeColorActivity.class, ChecksumActivity.class, ChecksumErrorActivity.class, + DeletionAcknowledgmentActivity.class, EditorActivity.class, FileActivity.class, FolderCreatedActivity.class, diff --git a/core/src/saros/concurrent/jupiter/internal/Jupiter.java b/core/src/saros/concurrent/jupiter/internal/Jupiter.java index 81922d4069..b36e252882 100644 --- a/core/src/saros/concurrent/jupiter/internal/Jupiter.java +++ b/core/src/saros/concurrent/jupiter/internal/Jupiter.java @@ -260,16 +260,20 @@ protected void checkPreconditions(JupiterVectorTime time) throws TransformationE if (!this.ackJupiterActivityList.isEmpty() && (time.getRemoteOperationCount() < this.ackJupiterActivityList.get(0).getLocalOperationCount())) { + // TODO improve exception message; what is precondition 1? throw new TransformationException("Precondition #1 violated."); } else if (time.getRemoteOperationCount() > this.vectorTime.getLocalOperationCount()) { throw new TransformationException( - "precondition #2 violated (Remote vector time is greater than local vector time)."); + "precondition #2 violated (Remote vector time is greater than local vector time) - remote time: " + + time + + " ,local time: " + + vectorTime); } else if (time.getLocalOperationCount() != this.vectorTime.getRemoteOperationCount()) { throw new TransformationException( - "Precondition #3 violated (Vector time does not match): " + "Precondition #3 violated (Vector time does not match) - remote time :" + time - + " , " - + this.vectorTime); + + " ,local time: " + + vectorTime); } } diff --git a/core/src/saros/concurrent/management/ConcurrentDocumentClient.java b/core/src/saros/concurrent/management/ConcurrentDocumentClient.java index 6b57e96aac..c440e91e3f 100644 --- a/core/src/saros/concurrent/management/ConcurrentDocumentClient.java +++ b/core/src/saros/concurrent/management/ConcurrentDocumentClient.java @@ -4,14 +4,13 @@ import java.util.List; import org.apache.log4j.Logger; import saros.activities.ChecksumActivity; -import saros.activities.FileActivity; import saros.activities.IActivity; -import saros.activities.IActivityReceiver; import saros.activities.JupiterActivity; import saros.activities.SPath; import saros.activities.TextEditActivity; import saros.concurrent.jupiter.Operation; import saros.concurrent.jupiter.TransformationException; +import saros.repackaged.picocontainer.Startable; import saros.session.ISarosSession; /** @@ -24,7 +23,7 @@ *
When JupiterActivities are received from the server they are transformed by the * ConcurrentDocumentClient to TextEditActivities which can then be executed locally. */ -public class ConcurrentDocumentClient { +public class ConcurrentDocumentClient implements Startable { private static Logger log = Logger.getLogger(ConcurrentDocumentClient.class); @@ -32,10 +31,23 @@ public class ConcurrentDocumentClient { private final JupiterClient jupiterClient; - public ConcurrentDocumentClient(ISarosSession sarosSession) { + private final ResourceActivityFilter resourceActivityFilter; + public ConcurrentDocumentClient(ISarosSession sarosSession) { this.sarosSession = sarosSession; this.jupiterClient = new JupiterClient(sarosSession); + + this.resourceActivityFilter = new ResourceActivityFilter(sarosSession, this::reset); + } + + @Override + public void start() { + resourceActivityFilter.initialize(); + } + + @Override + public void stop() { + resourceActivityFilter.dispose(); } /** @@ -68,6 +80,9 @@ public IActivity transformToJupiter(IActivity activity) { return jupiterClient.withTimestamp(checksumActivity); } else { + resourceActivityFilter.handleFileDeletion(activity); + resourceActivityFilter.handleFileCreation(activity); + return activity; } } @@ -78,6 +93,9 @@ public IActivity transformToJupiter(IActivity activity) { *
This method will transform them back from Jupiter-specific activities to locally executable * activities. @GUI Must be called on the GUI Thread to ensure proper synchronization * + *
Drops activities that are reported as filtered out by {@link
+ * ResourceActivityFilter#isFiltered(IActivity)}.
+ *
* @host and @client This is called whenever activities are received from REMOTELY both on the
* client and on the host
* @param activity The activity to be transformed
@@ -91,7 +109,13 @@ public List Drops activities that are reported as filtered out by {@link
+ * ResourceActivityFilter#isFiltered(IActivity)}.
+ *
* @host
* @sarosThread Must be executed in the Saros dispatch thread.
* @notGUI This method may not be called from SWT, otherwise a deadlock might occur!!
@@ -104,9 +109,13 @@ public List The set is updated once the file is recreated as we then want to handle activities for it
+ * again. Furthermore, it is updated once all acknowledgments for a file deletion were received as
+ * the filter is then no longer necessary.
+ *
+ * This way of filtering can lead to issues when the order of activities is not preserved, e.g.
+ * when we receive the content change for a new file is received before the file creation. Such
+ * activities will be dropped, leading to inconsistencies.
+ */
+ private final Map Does nothing if the passed activity is not a {@link FileActivity} or does not have the type
+ * {@link FileActivity.Type#REMOVED} or {@link FileActivity.Type#MOVED}.
+ *
+ * @param activity the activity to handle
+ * @see #deletedFileFilter
+ * @see #handleFileCreation(IActivity)
+ */
+ void handleFileDeletion(IActivity activity) {
+ if (!(activity instanceof FileActivity)) {
+ return;
+ }
+
+ FileActivity fileActivity = (FileActivity) activity;
+
+ SPath removedFile;
+ if (fileActivity.getType() == FileActivity.Type.REMOVED) {
+ removedFile = fileActivity.getPath();
+
+ } else if (fileActivity.getType() == FileActivity.Type.MOVED
+ && !fileActivity.getPath().equals(fileActivity.getOldPath())) {
+
+ removedFile = fileActivity.getOldPath();
+
+ } else {
+ return;
+ }
+
+ List Does nothing if the passed activity is not a {@link FileActivity} or does not have the type
+ * {@link FileActivity.Type#CREATED} or {@link FileActivity.Type#MOVED}.
+ *
+ * @param activity the activity to handle
+ * @see #deletedFileFilter
+ * @see #handleFileDeletion(IActivity)
+ */
+ void handleFileCreation(IActivity activity) {
+ if (!(activity instanceof FileActivity)) {
+ return;
+ }
+
+ FileActivity fileActivity = (FileActivity) activity;
+
+ if (fileActivity.getType() == FileActivity.Type.MOVED
+ || fileActivity.getType() == FileActivity.Type.CREATED) {
+
+ SPath addedFile = fileActivity.getPath();
+
+ if (deletedFileFilter.containsKey(addedFile)) {
+ log.debug("Removing activity filter for re-created file " + addedFile);
+
+ deletedFileFilter.remove(addedFile);
+ }
+ }
+ }
+
+ /**
+ * Returns whether the passed activity is filtered out. This is determined by the held map of
+ * deleted files.
+ *
+ * Non-resource activities are never detected as being filtered. Furthermore, resource
+ * activities dealing with the tear-down of the internal Saros state related to the deleted
+ * resource are never detected as being filtered. This applies to the following kinds of
+ * activities:
+ *
+ * Activities that contain file deletions are {@link FileActivity file activities} of the type
+ * {@link Type#REMOVED} or {@link Type#MOVED}.
+ *
+ * @param fileActivity the file activity to check and acknowledge if applicable
+ */
+ private void acknowledgeDeletionActivity(FileActivity fileActivity) {
+ SPath deletedResource;
+
+ if (fileActivity.getType() == Type.MOVED) {
+ deletedResource = fileActivity.getOldPath();
+
+ } else if (fileActivity.getType() == Type.REMOVED) {
+ deletedResource = fileActivity.getPath();
+
+ } else {
+ return;
+ }
+
+ User localUser = sarosSession.getLocalUser();
+
+ IActivity deletionAcknowledgementActivity =
+ new DeletionAcknowledgmentActivity(localUser, deletedResource);
+
+ log.debug("Sending deletion acknowledgment for " + deletedResource);
+ fireActivity(deletionAcknowledgementActivity);
+ }
+}
diff --git a/intellij/src/saros/intellij/eventhandler/filesystem/LocalFilesystemModificationHandler.java b/intellij/src/saros/intellij/eventhandler/filesystem/LocalFilesystemModificationHandler.java
index 20f7438d0c..42bcd57a4f 100644
--- a/intellij/src/saros/intellij/eventhandler/filesystem/LocalFilesystemModificationHandler.java
+++ b/intellij/src/saros/intellij/eventhandler/filesystem/LocalFilesystemModificationHandler.java
@@ -475,8 +475,6 @@ private void generateFileDeletionActivity(VirtualFile deletedFile) {
cleanUpDeletedFileState(path);
dispatchActivity(activity);
-
- // TODO reset the vector time for the deleted file or contained files if folder
}
/**
@@ -835,15 +833,11 @@ private void generateFileMoveActivity(
dispatchActivity(activity);
- if (oldPathIsShared) {
- if (fileIsOpen) {
- EditorActivity closeOldEditorActivity =
- new EditorActivity(user, EditorActivity.Type.CLOSED, oldFilePath);
-
- dispatchActivity(closeOldEditorActivity);
- }
+ if (oldPathIsShared && fileIsOpen) {
+ EditorActivity closeOldEditorActivity =
+ new EditorActivity(user, EditorActivity.Type.CLOSED, oldFilePath);
- // TODO reset the vector time for the old file
+ dispatchActivity(closeOldEditorActivity);
}
if (newPathIsShared && fileIsOpen) {
+ *
+ *
+ * @param activity the activity to check
+ * @return whether the passed activity is filtered out
+ * @see #handleFileDeletion(IActivity)
+ * @see #handleFileCreation(IActivity)
+ */
+ boolean isFiltered(IActivity activity) {
+
+ if (!(activity instanceof IResourceActivity)
+ || activity instanceof DeletionAcknowledgmentActivity) {
+
+ return false;
+ }
+
+ SPath path = ((IResourceActivity) activity).getPath();
+
+ if (path == null) {
+ return false;
+ }
+
+ boolean pathIsFiltered = deletedFileFilter.containsKey(path);
+
+ if (pathIsFiltered) {
+ if (activity instanceof ChecksumActivity) {
+ ChecksumActivity checksumActivity = (ChecksumActivity) activity;
+
+ return checksumActivity.getHash() != ChecksumActivity.NON_EXISTING_DOC
+ && checksumActivity.getLength() != ChecksumActivity.NON_EXISTING_DOC;
+
+ } else if (activity instanceof EditorActivity) {
+ EditorActivity editorActivity = (EditorActivity) activity;
+
+ return EditorActivity.Type.CLOSED != editorActivity.getType();
+ }
+ }
+
+ return pathIsFiltered;
+ }
+
+ /** Initializes all contained components. */
+ public void initialize() {
+ sarosSession.addActivityConsumer(activityConsumer, Priority.PASSIVE);
+ sarosSession.addListener(sessionListener);
+ }
+
+ /** Disposes all contained components to prepare them for garbage collection. */
+ public void dispose() {
+ sarosSession.removeActivityConsumer(activityConsumer);
+ sarosSession.removeListener(sessionListener);
+ }
+}
diff --git a/core/src/saros/session/SarosCoreSessionContextFactory.java b/core/src/saros/session/SarosCoreSessionContextFactory.java
index ae9aab4ac3..7dfa819e3a 100644
--- a/core/src/saros/session/SarosCoreSessionContextFactory.java
+++ b/core/src/saros/session/SarosCoreSessionContextFactory.java
@@ -15,6 +15,7 @@
import saros.session.internal.ActivityHandler;
import saros.session.internal.ActivitySequencer;
import saros.session.internal.ChangeColorManager;
+import saros.session.internal.DeletionAcknowledgmentDispatcher;
import saros.session.internal.LeaveAndKickHandler;
import saros.session.internal.PermissionManager;
import saros.session.internal.UserInformationHandler;
@@ -58,6 +59,7 @@ public final void createComponents(ISarosSession session, MutablePicoContainer c
container.addComponent(ActivityHandler.class);
container.addComponent(ActivitySequencer.class);
container.addComponent(ChangeColorManager.class);
+ container.addComponent(DeletionAcknowledgmentDispatcher.class);
container.addComponent(FollowModeManager.class);
container.addComponent(FollowModeBroadcaster.class);
container.addComponent(LeaveAndKickHandler.class);
diff --git a/core/src/saros/session/internal/ActivityHandler.java b/core/src/saros/session/internal/ActivityHandler.java
index 384d629632..ff971037cd 100644
--- a/core/src/saros/session/internal/ActivityHandler.java
+++ b/core/src/saros/session/internal/ActivityHandler.java
@@ -401,7 +401,7 @@ private TransformationResult directServerActivities(List