Bug 489540 - Error marker for unknown nature referenced by project

Change-Id: I2aaac4bf01dd3099ec926faebd88b854c6b58476
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.core.resources/plugin.properties b/bundles/org.eclipse.core.resources/plugin.properties
index 1c45ea3..1497e27 100644
--- a/bundles/org.eclipse.core.resources/plugin.properties
+++ b/bundles/org.eclipse.core.resources/plugin.properties
@@ -38,3 +38,4 @@
 regexFilterProvider.name = Regular Expression
 
 trace.component.label = Platform Core Resources
+unknownNatureMarkerName=Unknown nature
diff --git a/bundles/org.eclipse.core.resources/plugin.xml b/bundles/org.eclipse.core.resources/plugin.xml
index 7f8663c..bdf6dfc 100644
--- a/bundles/org.eclipse.core.resources/plugin.xml
+++ b/bundles/org.eclipse.core.resources/plugin.xml
@@ -257,4 +257,18 @@
          </bundle>
       </component>
    </extension>
+   <extension
+         id="unknownNature"
+         name="%unknownNatureMarkerName"
+         point="org.eclipse.core.resources.markers">
+      <super
+            type="org.eclipse.core.resources.problemmarker">
+      </super>
+      <persistent
+            value="true">
+      </persistent>
+      <attribute
+            name="natureId">
+      </attribute>
+   </extension>
 </plugin>
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/CheckMissingNaturesListener.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/CheckMissingNaturesListener.java
new file mode 100644
index 0000000..4e19902
--- /dev/null
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/CheckMissingNaturesListener.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Red Hat 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:
+ *     Mickael Istria (Red Hat Inc.) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.core.internal.resources;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.core.internal.utils.Messages;
+import org.eclipse.core.resources.*;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.osgi.util.NLS;
+
+public class CheckMissingNaturesListener implements IResourceChangeListener {
+
+	public static final String MARKER_TYPE = ResourcesPlugin.getPlugin().getBundle().getSymbolicName() + ".unknownNature"; //$NON-NLS-1$
+	public static final String NATURE_ID_ATTRIBUTE = "natureId"; //$NON-NLS-1$
+
+	@Override
+	public void resourceChanged(IResourceChangeEvent event) {
+		if (event.getDelta() == null) {
+			return;
+		}
+		try {
+			event.getDelta().accept(new IResourceDeltaVisitor() {
+				@Override
+				public boolean visit(IResourceDelta delta) throws CoreException {
+					if (delta.getResource() != null && delta.getResource().getType() == IResource.PROJECT && (delta.getKind() == IResourceDelta.ADDED || delta.getKind() == IResourceDelta.CHANGED)) {
+						final IProject project = (IProject) delta.getResource();
+						if (!project.isAccessible()) {
+							return false;
+						}
+						int severity = getMissingNatureSeverity(project);
+						if (severity < 0) {
+							return false;
+						}
+
+						final Set<String> missingNatures = new HashSet<>();
+						for (String natureId : project.getDescription().getNatureIds()) {
+							if (project.getWorkspace().getNatureDescriptor(natureId) == null) {
+								missingNatures.add(natureId);
+							}
+						}
+						final Set<IMarker> toRemove = new HashSet<>();
+						for (IMarker existingMarker : project.findMarkers(MARKER_TYPE, true, IResource.DEPTH_ZERO)) {
+							String markerNature = existingMarker.getAttribute(NATURE_ID_ATTRIBUTE, ""); //$NON-NLS-1$
+							if (!missingNatures.contains(markerNature)) {
+								toRemove.add(existingMarker);
+							} else {
+								// no need to create a new marker
+								missingNatures.remove(markerNature);
+							}
+						}
+						if (!toRemove.isEmpty() || !missingNatures.isEmpty()) {
+							WorkspaceJob workspaceJob = new WorkspaceJob(NLS.bind(Messages.addingMissingNatureMarkersOnProject, project.getName())) {
+								@Override
+								public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException {
+									for (IMarker marker : toRemove) {
+										marker.delete();
+									}
+									for (String natureId : missingNatures) {
+										IMarker marker = project.createMarker(MARKER_TYPE);
+										marker.setAttribute(IMarker.SEVERITY, severity);
+										marker.setAttribute(IMarker.MESSAGE, NLS.bind(Messages.natures_missingNature, natureId));
+										marker.setAttribute(NATURE_ID_ATTRIBUTE, natureId);
+									}
+									return Status.OK_STATUS;
+								}
+
+								@Override
+								public boolean belongsTo(Object family) {
+									return super.belongsTo(family) || MARKER_TYPE.equals(family);
+								}
+							};
+							workspaceJob.setUser(false);
+							workspaceJob.setSystem(true);
+							workspaceJob.setPriority(Job.DECORATE);
+							workspaceJob.schedule();
+						}
+					}
+					return delta.getResource() == null || delta.getResource().getType() == IResource.ROOT;
+				}
+
+				private int getMissingNatureSeverity(final IProject project) {
+					int severity = PreferenceInitializer.PREF_MISSING_NATURE_MARKER_SEVERITY_DEFAULT;
+					IEclipsePreferences node = InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES);
+					if (node != null) {
+						severity = node.getInt(ResourcesPlugin.PREF_MISSING_NATURE_MARKER_SEVERITY, PreferenceInitializer.PREF_MISSING_NATURE_MARKER_SEVERITY_DEFAULT);
+					}
+					return severity;
+				}
+			});
+		} catch (CoreException e) {
+			ResourcesPlugin.getPlugin().getLog().log(new Status(IStatus.ERROR, ResourcesPlugin.getPlugin().getBundle().getSymbolicName(), e.getMessage(), e));
+		}
+
+	}
+
+}
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/PreferenceInitializer.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/PreferenceInitializer.java
index 3ae12e1..8cd0510 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/PreferenceInitializer.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/PreferenceInitializer.java
@@ -21,7 +21,7 @@
 
 	// internal preference keys
 	public static final String PREF_OPERATIONS_PER_SNAPSHOT = "snapshots.operations"; //$NON-NLS-1$
-	public static final String PREF_DELTA_EXPIRATION = "delta.expiration";  //$NON-NLS-1$
+	public static final String PREF_DELTA_EXPIRATION = "delta.expiration"; //$NON-NLS-1$
 
 	// DEFAULTS
 	public static final boolean PREF_AUTO_REFRESH_DEFAULT = false;
@@ -39,6 +39,13 @@
 	public static final long PREF_MAX_FILE_STATE_SIZE_DEFAULT = 1024 * 1024l; // 1 MB
 	public static final int PREF_MAX_FILE_STATES_DEFAULT = 50;
 	public static final long PREF_DELTA_EXPIRATION_DEFAULT = 30 * 24 * 3600 * 1000l; // 30 days
+	/**
+	 * Default setting for {@value ResourcesPlugin#PREF_MISSING_NATURE_MARKER_SEVERITY}.
+	 * Currently -1/ignore, but very likely to change.
+	 *
+	 * @since 3.12
+	 */
+	public static final int PREF_MISSING_NATURE_MARKER_SEVERITY_DEFAULT = -1;
 
 	public PreferenceInitializer() {
 		super();
@@ -59,6 +66,7 @@
 		node.put(ResourcesPlugin.PREF_BUILD_ORDER, PREF_BUILD_ORDER_DEFAULT);
 		node.putInt(ResourcesPlugin.PREF_MAX_BUILD_ITERATIONS, PREF_MAX_BUILD_ITERATIONS_DEFAULT);
 		node.putBoolean(ResourcesPlugin.PREF_DEFAULT_BUILD_ORDER, PREF_DEFAULT_BUILD_ORDER_DEFAULT);
+		node.putInt(ResourcesPlugin.PREF_MISSING_NATURE_MARKER_SEVERITY, PREF_MISSING_NATURE_MARKER_SEVERITY_DEFAULT);
 
 		// history store defaults
 		node.putBoolean(ResourcesPlugin.PREF_APPLY_FILE_STATE_POLICY, PREF_APPLY_FILE_STATE_POLICY_DEFAULT);
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java
index 34e8a2b..735b53b 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/Messages.java
@@ -326,6 +326,9 @@
 	public static String WM_nativeErr;
 	public static String WM_mutexAbandoned;
 
+	public static String cleanMarkersOnProject;
+	public static String addingMissingNatureMarkersOnProject;
+
 	static {
 		// initialize resource bundles
 		NLS.initializeMessages(BUNDLE_NAME, Messages.class);
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties
index eadd09c..94e9d75 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/utils/messages.properties
@@ -118,6 +118,9 @@
 preferences_saveProblems=Exception occurred while saving project preferences: {0}.
 preferences_syncException=Exception occurred while synchronizing node: {0}.
 
+cleanMarkersOnProject=Clear natures markers on {0}
+addingMissingNatureMarkersOnProject=Add missing natures markers on {0}
+
 projRead_badLinkName = Names ''{0}'' and ''{1}'' detected for a single link.  Using ''{0}''.
 projRead_badLinkType2 = Types ''{0}'' and ''{1}'' detected for a single link.  Using ''{0}''.
 projRead_badLocation = Locations ''{0}'' and ''{1}'' detected for a single link.  Using ''{0}''.
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java
index 59d44a9..a186eef 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/ResourcesPlugin.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- *  Copyright (c) 2000, 2015 IBM Corporation and others.
+ *  Copyright (c) 2000, 2016 IBM Corporation 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
@@ -327,6 +327,14 @@
 	public static final boolean DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS = false;
 
 	/**
+	 * Name of a preference for configuring the marker severity in case project
+	 * description references an unknown nature.
+	 *
+	 * @since 3.12
+	 */
+	public static final String PREF_MISSING_NATURE_MARKER_SEVERITY = "missingNatureMarkerSeverity"; //$NON-NLS-1$
+
+	/**
 	 * The single instance of this plug-in runtime class.
 	 */
 	private static ResourcesPlugin plugin;
@@ -340,6 +348,8 @@
 	private ServiceRegistration<IWorkspace> workspaceRegistration;
 	private ServiceRegistration<DebugOptionsListener> debugRegistration;
 
+	private CheckMissingNaturesListener checkMissingNaturesListener;
+
 	/**
 	 * Constructs an instance of this plug-in runtime class.
 	 * <p>
@@ -429,6 +439,7 @@
 		if (workspace == null) {
 			return;
 		}
+		workspace.removeResourceChangeListener(checkMissingNaturesListener);
 		if (workspaceRegistration != null) {
 			workspaceRegistration.unregister();
 		}
@@ -467,6 +478,8 @@
 		IStatus result = workspace.open(null);
 		if (!result.isOK())
 			getLog().log(result);
+		checkMissingNaturesListener = new CheckMissingNaturesListener();
+		workspace.addResourceChangeListener(checkMissingNaturesListener, IResourceChangeEvent.POST_CHANGE);
 		workspaceRegistration = context.registerService(IWorkspace.class, workspace, null);
 	}
 
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/NatureTest.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/NatureTest.java
index a2b4c4e..36c5fb5 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/NatureTest.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/NatureTest.java
@@ -22,7 +22,9 @@
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.jobs.IJobManager;
 import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.core.tests.internal.resources.SimpleNature;
+import org.junit.Assert;
 
 /**
  * Tests all aspects of project natures.  These tests only
@@ -30,6 +32,8 @@
  * APIs on IWorkspace are tested by IWorkspaceTest.
  */
 public class NatureTest extends ResourceTest {
+	private static final String MISSING_NATURE_MARKER_ID = "org.eclipse.core.resources.unknownNature";
+
 	/**
 	 * Constructor for NatureTest.
 	 */
@@ -84,11 +88,20 @@
 
 	@Override
 	protected void tearDown() throws Exception {
+		InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES).remove(ResourcesPlugin.PREF_MISSING_NATURE_MARKER_SEVERITY);
+		InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES).flush();
 		super.tearDown();
 		getWorkspace().getRoot().refreshLocal(IResource.DEPTH_INFINITE, null);
 		ensureDoesNotExistInWorkspace(getWorkspace().getRoot());
 	}
 
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES).putInt(ResourcesPlugin.PREF_MISSING_NATURE_MARKER_SEVERITY, IMarker.SEVERITY_WARNING);
+		InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES).flush();
+	}
+
 	/**
 	 * Tests invalid additions to the set of natures for a project.
 	 */
@@ -421,4 +434,33 @@
 		assertTrue("4.0", project.hasNature(NATURE_SIMPLE));
 		assertTrue("5.0", project.isNatureEnabled(NATURE_SIMPLE));
 	}
+
+	public void testMissingNatureAddsMarker() throws Exception {
+		IWorkspace ws = ResourcesPlugin.getWorkspace();
+		IProject project = ws.getRoot().getProject(getUniqueString());
+		ensureExistsInWorkspace(project, true);
+		IProjectDescription desc = project.getDescription();
+		desc.setNatureIds(new String[] {NATURE_MISSING});
+		project.setDescription(desc, IResource.FORCE | IResource.AVOID_NATURE_CONFIG, getMonitor());
+		project.refreshLocal(IResource.DEPTH_INFINITE, getMonitor());
+		project.build(IncrementalProjectBuilder.FULL_BUILD, getMonitor());
+		Job.getJobManager().join(MISSING_NATURE_MARKER_ID, getMonitor());
+		IMarker[] markers = project.findMarkers(MISSING_NATURE_MARKER_ID, false, IResource.DEPTH_ZERO);
+		Assert.assertEquals(1, markers.length);
+		IMarker marker = markers[0];
+		Assert.assertEquals(NATURE_MISSING, marker.getAttribute("natureId"));
+	}
+
+	public void testKnownNatureDoesntAddMarker() throws Exception {
+		IWorkspace ws = ResourcesPlugin.getWorkspace();
+		IProject project = ws.getRoot().getProject(getUniqueString());
+		ensureExistsInWorkspace(project, true);
+		IProjectDescription desc = project.getDescription();
+		desc.setNatureIds(new String[] {NATURE_SIMPLE});
+		project.setDescription(desc, getMonitor());
+		project.refreshLocal(IResource.DEPTH_INFINITE, getMonitor());
+		project.build(IncrementalProjectBuilder.FULL_BUILD, getMonitor());
+		Job.getJobManager().join(MISSING_NATURE_MARKER_ID, getMonitor());
+		Assert.assertEquals(0, project.findMarkers(MISSING_NATURE_MARKER_ID, false, IResource.DEPTH_ZERO).length);
+	}
 }