Bug 576169 - Distinguish between project removal from workspace/disk

On deleting a project from Package Explorer one can have two options:
(1) Remove from the workspace ("Delete project contents on disk (cannot
be undone)" checkbox is *not* selected)
(2) Remove from the workspace and disk ("Delete project contents on disk
(cannot be undone)" checkbox is selected)

This change adds a new flag to the IResourceDelta and a new method to
IResourceChangeDescriptionFactory to differentiate the
"type" of delete ((1) or (2)) in the delta objects created by
IResourceChangeDescriptionFactory.

ResourceChangeDescriptionFactory is updated to to set this flag
accordingly & tests added for new behavior.

Change-Id: Id3036344d864fb57fd1c514dbba3780df93adbbf
Signed-off-by: Mykola Zakharchuk <zakharchuk.vn@gmail.com>
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.resources/+/185701
Tested-by: Platform Bot <platform-bot@eclipse.org>
Reviewed-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF b/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF
index 81cb4d2..ca6d01c 100644
--- a/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.core.resources/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.core.resources; singleton:=true
-Bundle-Version: 3.15.100.qualifier
+Bundle-Version: 3.16.0.qualifier
 Bundle-Activator: org.eclipse.core.resources.ResourcesPlugin
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/mapping/ResourceChangeDescriptionFactory.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/mapping/ResourceChangeDescriptionFactory.java
index ef5c9e5..7cd068e 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/mapping/ResourceChangeDescriptionFactory.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/mapping/ResourceChangeDescriptionFactory.java
@@ -26,12 +26,25 @@
 	private ProposedResourceDelta root = new ProposedResourceDelta(ResourcesPlugin.getWorkspace().getRoot());
 
 	/**
-	 * Creates and a delta representing a deleted resource, and adds it to the provided
+	 * Creates a delta representing a deleted resource, and adds it to the provided
 	 * parent delta.
 	 * @param parentDelta The parent of the deletion delta to create
 	 * @param resource The deleted resource to create a delta for
 	 */
 	private ProposedResourceDelta buildDeleteDelta(ProposedResourceDelta parentDelta, IResource resource) {
+		return buildDeleteDelta(parentDelta, resource, false);
+	}
+
+	/**
+	 * Creates a delta representing a deleted resource, and adds it to the provided
+	 * parent delta.
+	 * @param parentDelta     The parent of the deletion delta to create
+	 * @param resource        The deleted resource to create a delta for
+	 * @param deleteContent <code>true</code> if resource should be also deleted
+	 *                        from the disk
+	 */
+	private ProposedResourceDelta buildDeleteDelta(ProposedResourceDelta parentDelta, IResource resource,
+			boolean deleteContent) {
 		//start with the existing delta for this resource, if any, to preserve other flags
 		ProposedResourceDelta delta = parentDelta.getChild(resource.getName());
 		if (delta == null) {
@@ -39,6 +52,8 @@
 			parentDelta.add(delta);
 		}
 		delta.setKind(IResourceDelta.REMOVED);
+		if (deleteContent)
+			delta.addFlags(IResourceDelta.DELETE_CONTENT_PROPOSED);
 		if (resource.getType() == IResource.FILE)
 			return delta;
 		//recurse to build deletion deltas for children
@@ -48,7 +63,7 @@
 			if (childCount > 0) {
 				ProposedResourceDelta[] childDeltas = new ProposedResourceDelta[childCount];
 				for (int i = 0; i < childCount; i++)
-					childDeltas[i] = buildDeleteDelta(delta, members[i]);
+					childDeltas[i] = buildDeleteDelta(delta, members[i], deleteContent);
 			}
 		} catch (CoreException e) {
 			//don't need to create deletion deltas for children of inaccessible resources
@@ -95,6 +110,11 @@
 		}
 	}
 
+	@Override
+	public void delete(IProject project, boolean deleteContent) {
+		buildDeleteDelta(root, project, deleteContent);
+	}
+
 	private void fail(CoreException e) {
 		Policy.log(e.getStatus().getSeverity(), "An internal error occurred while accumulating a change description.", e); //$NON-NLS-1$
 	}
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceDelta.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceDelta.java
index bb7989f..64ecf58 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceDelta.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IResourceDelta.java
@@ -203,6 +203,19 @@
 	int DERIVED_CHANGED = 0x400000;
 
 	/**
+	 * Change constant (bit mask) indicating that the content of the resource is
+	 * proposed to be deleted. This flag can only be found in deltas created by
+	 * {@link IResourceChangeDescriptionFactory} and indicates that the underlined
+	 * file object is proposed to be deleted from the file system (as opposite to
+	 * the change where only workspace model is deleted).
+	 *
+	 * @see IResourceChangeDescriptionFactory
+	 * @see IResourceDelta#getFlags()
+	 * @since 3.16
+	 */
+	int DELETE_CONTENT_PROPOSED = 0x800000;
+
+	/**
 	 * Accepts the given visitor.
 	 * The only kinds of resource deltas visited
 	 * are <code>ADDED</code>, <code>REMOVED</code>,
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/mapping/IResourceChangeDescriptionFactory.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/mapping/IResourceChangeDescriptionFactory.java
index 374d711..1832963 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/mapping/IResourceChangeDescriptionFactory.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/mapping/IResourceChangeDescriptionFactory.java
@@ -18,11 +18,16 @@
 import org.eclipse.core.runtime.IProgressMonitor;
 
 /**
- * This factory is used to build a resource delta that represents a proposed change
- * that can then be passed to the {@link ResourceChangeValidator#validateChange(IResourceDelta, IProgressMonitor)}
- * method in order to validate the change with any model providers stored in those resources.
- * The deltas created by calls to the methods of this interface will be the same as
- * those generated by the workspace if the proposed operations were performed.
+ * This factory is used to build a resource delta that represents a proposed
+ * change that can then be passed to the
+ * {@link ResourceChangeValidator#validateChange(IResourceDelta, IProgressMonitor)}
+ * method in order to validate the change with any model providers stored in
+ * those resources. The deltas created by calls to the methods of this interface
+ * will be the same as those generated by the workspace if the proposed
+ * operations were performed, except an additional
+ * {@link IResourceDelta#DELETE_CONTENT_PROPOSED} flag can indicate if the
+ * change is going to delete resources not only from the workspace model but
+ * also physically, so it cannot be undone.
  * <p>
  * This factory does not validate that the proposed operation is valid given the current
  * state of the resources and any other proposed changes. It only records the
@@ -69,6 +74,17 @@
 	void delete(IResource resource);
 
 	/**
+	 * Record the set of deltas representing a deletion of the given project.
+	 *
+	 * @param project       the project that will be deleted
+	 * @param deleteContent <code>true</code> if the project content on the disk
+	 *                      should be deleted. The content delete is not undoable.
+	 * @since 3.16
+	 * @see IResourceDelta#DELETE_CONTENT_PROPOSED
+	 */
+	void delete(IProject project, boolean deleteContent);
+
+	/**
 	 * Return the proposed delta that has been accumulated by this factory.
 	 * @return the proposed delta that has been accumulated by this factory
 	 */
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/AllTests.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/AllTests.java
index fe35c65..0b84be6 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/AllTests.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/AllTests.java
@@ -23,6 +23,9 @@
  * @since 3.2
  */
 @RunWith(Suite.class)
-@Suite.SuiteClasses({ ChangeValidationTest.class })
+@Suite.SuiteClasses({ 
+	ChangeValidationTest.class,
+	TestProjectDeletion.class
+	})
 public class AllTests {
 }
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/ChangeValidationTest.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/ChangeValidationTest.java
index f062187..ef4b334 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/ChangeValidationTest.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/ChangeValidationTest.java
@@ -210,6 +210,37 @@
 		assertStatusEqual(status, new String[] {ChangeDescription.getMessageFor(ChangeDescription.REMOVED, project)});
 	}
 
+	public void testProjectDeletionWithContents() {
+		factory.delete(project, true);
+		IStatus status = validateChange(factory);
+		assertStatusEqual(status, new String[] { ChangeDescription.getMessageFor(ChangeDescription.REMOVED, project) });
+		// Check if the given delta also indicates contents deletion
+		try {
+			TestModelProvider.checkContentsDeletion = true;
+			status = validateChange(factory);
+			assertEquals("Validation should return error status on contents deletion.", IStatus.ERROR,
+				status.getSeverity());
+		} finally {
+			TestModelProvider.checkContentsDeletion = false;
+		}
+	}
+
+	public void testProjectDeletionWithoutContents() {
+		factory.delete(project, false);
+		IStatus status = validateChange(factory);
+		assertStatusEqual(status, new String[] { ChangeDescription.getMessageFor(ChangeDescription.REMOVED, project) });
+		// Check if the given delta does not indicate contents removal
+		try {
+			TestModelProvider.checkContentsDeletion = true;
+			status = validateChange(factory);
+			assertEquals("Validation should return warning status on project removal from workspace.", IStatus.WARNING,
+				status.getSeverity());
+		} finally {
+			TestModelProvider.checkContentsDeletion = false;
+		}
+
+	}
+
 	public void testProjectMove() {
 		factory.move(project, new Path("MovedProject"));
 		IStatus status = validateChange(factory);
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestModelProvider.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestModelProvider.java
index 5671dc5..61dc2e7 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestModelProvider.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestModelProvider.java
@@ -28,6 +28,14 @@
 	 */
 	public static boolean enabled = false;
 
+	/**
+	 * Flag to check if the given delta proposes contents deletion. If set to
+	 * {@code true}, the {@link #validateChange(IResourceDelta, IProgressMonitor)}
+	 * will add {@link IStatus.ERROR} to the change description to indicate contents
+	 * deletion proposal.
+	 */
+	public static boolean checkContentsDeletion;
+
 	public static final String ID = "org.eclipse.core.tests.resources.modelProvider";
 
 	@Override
@@ -46,6 +54,17 @@
 		} catch (CoreException e) {
 			description.addError(e);
 		}
+
+		if (checkContentsDeletion) {
+			for (IResourceDelta resourceDelta : rootDelta.getAffectedChildren()) {
+				if ((resourceDelta.getFlags() & IResourceDelta.DELETE_CONTENT_PROPOSED) != 0) {
+					// With error status we indicate contents deletion proposal in given delta.
+					description.addError(new CoreException(
+							new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, "Contents deletion proposed.")));
+				}
+			}
+		}
+
 		return description.asStatus();
 	}
 }
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestProjectDeletion.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestProjectDeletion.java
new file mode 100644
index 0000000..1378121
--- /dev/null
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/mapping/TestProjectDeletion.java
@@ -0,0 +1,76 @@
+/*******************************************************************************
+ *  Copyright (c) 2021 IBM Corporation and others.
+ *
+ *  This program and the accompanying materials
+ *  are made available under the terms of the Eclipse Public License 2.0
+ *  which accompanies this distribution, and is available at
+ *  https://www.eclipse.org/legal/epl-2.0/
+ *
+ *  SPDX-License-Identifier: EPL-2.0
+ *
+ *  Contributors:
+ *     Mykola Zakharchuk <zakharchuk.vn@gmail.com> - Bug 576169
+ *******************************************************************************/
+package org.eclipse.core.tests.internal.mapping;
+
+import org.eclipse.core.resources.*;
+import org.eclipse.core.resources.mapping.IResourceChangeDescriptionFactory;
+import org.eclipse.core.resources.mapping.ResourceChangeValidator;
+import org.eclipse.core.tests.resources.ResourceTest;
+import org.junit.Test;
+
+/**
+ * Test to validate project kind and flags on deletion.
+ */
+public class TestProjectDeletion extends ResourceTest {
+	private IResourceChangeDescriptionFactory factory;
+	private IProject project;
+	private static int MASK = 0xFFFFFF;
+	private static int KIND_MASK = 0xFF;
+	private static int FLAGS_MASK = MASK ^= KIND_MASK;
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		project = getWorkspace().getRoot().getProject("Project");
+		IResource[] resources = buildResources(project, new String[] { "a/", "a/b/", "a/c/", "a/d", "a/b/e", "a/b/f" });
+		ensureExistsInWorkspace(resources, true);
+		assertExistsInWorkspace(resources);
+		factory = ResourceChangeValidator.getValidator().createDeltaFactory();
+		int kind = factory.getDelta().getKind();
+		int flags = factory.getDelta().getFlags();
+		assertEquals("Projects delta kind should not contain any bits before refactoring.", 0, kind &= ~KIND_MASK);
+		assertEquals("Projects delta flags should not be set before refactoring.", 0, flags &= ~FLAGS_MASK);
+	}
+
+	@Test
+	public void testDeletionWithContents() {
+		testDeletion(true);
+	}
+
+	@Test
+	public void testDeletionWithoutContents() {
+		testDeletion(false);
+	}
+
+	private void testDeletion(boolean deleteContents) {
+		factory.delete(project, deleteContents);
+		checkAffectedChildrenStatus(factory.getDelta().getAffectedChildren(), deleteContents);
+	}
+
+	private void checkAffectedChildrenStatus(IResourceDelta[] affectedChildren, boolean deleteContents) {
+		for (IResourceDelta iResourceDelta : affectedChildren) {
+			assertEquals("IResourceDelta.REMOVED kind is expected on project deletion.", IResourceDelta.REMOVED,
+					iResourceDelta.getKind());
+			if (deleteContents) {
+				assertEquals("IResourceDelta.DELETE_CONTENT_PROPOSED flag should be set on project contents deletion.",
+						IResourceDelta.DELETE_CONTENT_PROPOSED,
+						iResourceDelta.getFlags());
+			} else {
+				assertEquals("No flags should be set on project deletion from workspace.", 0,
+						iResourceDelta.getFlags());
+			}
+			checkAffectedChildrenStatus(iResourceDelta.getAffectedChildren(), deleteContents);
+		}
+	}
+}