Fixed Bug 67884 - Content type change events not reported
diff --git a/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentType.java b/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentType.java
index 403b2dd..c140b4f 100644
--- a/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentType.java
+++ b/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentType.java
@@ -170,12 +170,16 @@
 		if (type != FILE_EXTENSION_SPEC && type != FILE_NAME_SPEC)
 			throw new IllegalArgumentException("Unknown type: " + type); //$NON-NLS-1$		
 		if (!internalAddFileSpec(fileSpec, type | SPEC_USER_DEFINED))
+			// the entry was already there, nothing to do...
 			return;
-		// persist using preferences
+		// notify listeners
+		manager.fireContentTypeChangeEvent(this);
+		// update preferences
 		String key = getPreferenceKey(type);
 		Preferences contentTypeNode = manager.getPreferences().node(getId());
 		final String[] userSet = internalGetFileSpecs(type | IGNORE_PRE_DEFINED);
 		contentTypeNode.put(key, toListString(userSet));
+		// persist preferences
 		try {
 			contentTypeNode.flush();
 		} catch (BackingStoreException bse) {
@@ -472,20 +476,19 @@
 		return false;
 	}
 
-	void internalRemoveFileSpec(String fileSpec, int typeMask) {
-		if (aliasTarget != null) {
-			aliasTarget.internalRemoveFileSpec(fileSpec, typeMask);
-			return;
-		}
+	boolean internalRemoveFileSpec(String fileSpec, int typeMask) {
+		if (aliasTarget != null)
+			return aliasTarget.internalRemoveFileSpec(fileSpec, typeMask);
 		if (fileSpecs == null)
-			return;
+			return false;
 		for (Iterator i = fileSpecs.iterator(); i.hasNext();) {
 			FileSpec spec = (FileSpec) i.next();
 			if ((spec.getType() == typeMask) && fileSpec.equals(spec.getText())) {
 				i.remove();
-				return;
+				return true;
 			}
 		}
+		return false;
 	}
 
 	private IContentDescriber invalidateDescriber(Throwable reason) {
@@ -533,8 +536,12 @@
 		}
 		if (type != FILE_EXTENSION_SPEC && type != FILE_NAME_SPEC)
 			throw new IllegalArgumentException("Unknown type: " + type); //$NON-NLS-1$
-		internalRemoveFileSpec(fileSpec, type | SPEC_USER_DEFINED);
-		// persist using preferences
+		if (!internalRemoveFileSpec(fileSpec, type | SPEC_USER_DEFINED))
+			// the entry was not there... nothing to do
+			return;
+		// notify listeners
+		manager.fireContentTypeChangeEvent(this);
+		// update preferences
 		String key = getPreferenceKey(type);
 		Preferences contentTypeNode = manager.getPreferences().node(getId());
 		final String[] userSet = internalGetFileSpecs(type | IGNORE_PRE_DEFINED);
@@ -542,6 +549,7 @@
 			contentTypeNode.remove(key);
 		else
 			contentTypeNode.put(key, toListString(userSet));
+		// persist using preferences
 		try {
 			contentTypeNode.flush();
 		} catch (BackingStoreException bse) {
@@ -567,13 +575,22 @@
 	 * (non-Javadoc) 
 	 * @see org.eclipse.core.runtime.content.IContentType#setDefaultCharset(java.lang.String)
 	 */
-	public void setDefaultCharset(String userCharset) throws CoreException {
-		this.userCharset = userCharset;
+	public void setDefaultCharset(String newCharset) throws CoreException {
+		if (userCharset == null) {
+			if (newCharset == null)
+				return;
+		} else if (userCharset.equals(newCharset))
+			return;
+		userCharset = newCharset;
+		// notify listeners
+		manager.fireContentTypeChangeEvent(this);
+		// update preferences
 		Preferences contentTypeNode = manager.getPreferences().node(getId());
 		if (userCharset == null)
 			contentTypeNode.remove(PREF_DEFAULT_CHARSET);
 		else
 			contentTypeNode.put(PREF_DEFAULT_CHARSET, userCharset);
+		// persist preferences
 		try {
 			contentTypeNode.flush();
 		} catch (BackingStoreException bse) {
diff --git a/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentTypeManager.java b/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentTypeManager.java
index 8f00c30..0b4f95a 100644
--- a/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentTypeManager.java
+++ b/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/ContentTypeManager.java
@@ -13,6 +13,7 @@
 import java.io.*;
 import java.util.*;
 import org.eclipse.core.internal.runtime.InternalPlatform;
+import org.eclipse.core.internal.runtime.ListenerList;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.*;
 import org.eclipse.core.runtime.preferences.InstanceScope;
@@ -28,6 +29,13 @@
 
 	private ContentTypeBuilder builder;
 	private Map catalog = new HashMap();
+	/** 
+	 * List of registered listeners (element type: 
+	 * <code>IContentTypeChangeListener</code>).
+	 * These listeners are to be informed when 
+	 * something in a content type changes.
+	 */
+	protected ListenerList contentTypeListeners = new ListenerList();
 
 	// a comparator used when resolving conflicts (two types associated to the same spec) 
 	private Comparator conflictComparator = new Comparator() {
@@ -375,15 +383,31 @@
 	 * @see IContentTypeManager#addContentTypeChangeListener(IContentTypeChangeListener)
 	 */
 	public void addContentTypeChangeListener(IContentTypeChangeListener listener) {
-		// TODO https://bugs.eclipse.org/bugs/show_bug.cgi?id=67884
-		// Content type change events not reported
+		contentTypeListeners.add(listener);
 	}
 
 	/* (non-Javadoc)
 	 * @see IContentTypeManager#removeContentTypeChangeListener(IContentTypeChangeListener)
 	 */
 	public void removeContentTypeChangeListener(IContentTypeChangeListener listener) {
-		// TODO https://bugs.eclipse.org/bugs/show_bug.cgi?id=67884
-		// Content type change events not reported
+		contentTypeListeners.remove(listener);
+	}
+
+	public void fireContentTypeChangeEvent(ContentType type) {
+		Object[] listeners = this.contentTypeListeners.getListeners();
+		for (int i = 0; i < listeners.length; i++) {
+			final ContentTypeChangeEvent event = new ContentTypeChangeEvent(type);
+			final IContentTypeChangeListener listener = (IContentTypeChangeListener) listeners[i];
+			ISafeRunnable job = new ISafeRunnable() {
+				public void handleException(Throwable exception) {
+					// already logged in Platform#run()
+				}
+
+				public void run() throws Exception {
+					listener.contentTypeChanged(event);
+				}
+			};
+			Platform.run(job);
+		}
 	}
 }
\ No newline at end of file
diff --git a/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/FileSpec.java b/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/FileSpec.java
index c689b5c..c0a0d4f 100644
--- a/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/FileSpec.java
+++ b/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/content/FileSpec.java
@@ -34,7 +34,7 @@
 		return type;
 	}
 
-	public int getBasicType() {
+	public static int getBasicType(int type) {
 		return BASIC_TYPE & type;
 	}
 
@@ -42,11 +42,11 @@
 		if (!(other instanceof FileSpec))
 			return false;
 		FileSpec otherFileSpec = (FileSpec) other;
-		return getBasicType() == otherFileSpec.getBasicType() && text.equalsIgnoreCase(otherFileSpec.text);
+		return equals(text, otherFileSpec.getType());
 	}
 
-	public boolean equals(String text, int basicType) {
-		return getBasicType() == basicType && this.text.equalsIgnoreCase(text);
+	public boolean equals(String text, int otherType) {
+		return getBasicType(type) == getBasicType(otherType) && this.text.equalsIgnoreCase(text);
 	}
 
 	public int hashCode() {
diff --git a/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/content/IContentTypeManagerTest.java b/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/content/IContentTypeManagerTest.java
index 9edd15a..21a34e3 100644
--- a/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/content/IContentTypeManagerTest.java
+++ b/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/content/IContentTypeManagerTest.java
@@ -11,11 +11,13 @@
 package org.eclipse.core.tests.runtime.content;
 
 import java.io.*;
+import java.util.*;
 import junit.framework.Test;
 import junit.framework.TestSuite;
 import org.eclipse.core.internal.content.*;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.*;
+import org.eclipse.core.runtime.content.IContentTypeManager.ContentTypeChangeEvent;
 import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.core.tests.harness.BundleTestingHelper;
 import org.eclipse.core.tests.harness.EclipseWorkspaceTest;
@@ -651,4 +653,89 @@
 			text.removeFileSpec("foo.bar", IContentType.FILE_NAME_SPEC);
 		}
 	}
+
+	class ContentTypeChangeTracer implements IContentTypeManager.IContentTypeChangeListener {
+		private Set changed = new HashSet();
+
+		public void contentTypeChanged(ContentTypeChangeEvent event) {
+			changed.add(event.getContentType());
+		}
+
+		public Collection getChanges() {
+			return changed;
+		}
+
+		public void reset() {
+			changed.clear();
+		}
+
+		public boolean isOnlyChange(IContentType myType) {
+			return changed.size() == 1 && changed.contains(myType);
+		}
+	}
+
+	public void testEvents() {
+		IContentTypeManager contentTypeManager = Platform.getContentTypeManager();
+		IContentType myType = contentTypeManager.getContentType(RuntimeTestsPlugin.PI_RUNTIME_TESTS + ".myContent");
+		assertNotNull("0.9", myType);
+
+		ContentTypeChangeTracer tracer;
+
+		tracer = new ContentTypeChangeTracer();
+		contentTypeManager.addContentTypeChangeListener(tracer);
+
+		// add a file spec and check event
+		try {
+			myType.addFileSpec("another.file.name", IContentType.FILE_NAME_SPEC);
+		} catch (CoreException e) {
+			fail("1.0", e);
+		}
+		assertTrue("1.1", tracer.isOnlyChange(myType));
+
+		// remove a non-existing file spec - should not cause an event to be fired
+		tracer.reset();
+		try {
+			myType.removeFileSpec("another.file.name", IContentType.FILE_EXTENSION_SPEC);
+		} catch (CoreException e) {
+			fail("2.0", e);
+		}
+		assertTrue("2.1", !tracer.isOnlyChange(myType));
+
+		// add a file spec again and check no event is generated
+		tracer.reset();
+		try {
+			myType.addFileSpec("another.file.name", IContentType.FILE_NAME_SPEC);
+		} catch (CoreException e) {
+			fail("3.0", e);
+		}
+		assertTrue("3.1", !tracer.isOnlyChange(myType));
+
+		// remove a file spec and check event
+		tracer.reset();
+		try {
+			myType.removeFileSpec("another.file.name", IContentType.FILE_NAME_SPEC);
+		} catch (CoreException e) {
+			fail("4.0", e);
+		}
+		assertTrue("4.1", tracer.isOnlyChange(myType));
+
+		// change the default charset and check event
+		tracer.reset();
+		try {
+			myType.setDefaultCharset("FOO");
+		} catch (CoreException e) {
+			fail("5.0", e);
+		}
+		assertTrue("5.1", tracer.isOnlyChange(myType));
+
+		// set the default charset to the same - no event should be generated
+		tracer.reset();
+		try {
+			myType.setDefaultCharset("FOO");
+		} catch (CoreException e) {
+			fail("6.0", e);
+		}
+		assertTrue("6.1", !tracer.isOnlyChange(myType));
+
+	}
 }
\ No newline at end of file