Bug 500892 - API to create/remove content-types in user-space

Change-Id: I925cdee45d97d7d657e0d25c80eb69c43e9c9d71
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.core.contenttype/META-INF/MANIFEST.MF b/bundles/org.eclipse.core.contenttype/META-INF/MANIFEST.MF
index 74b6163..65c5da8 100644
--- a/bundles/org.eclipse.core.contenttype/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.core.contenttype/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.core.contenttype; singleton:=true
-Bundle-Version: 3.5.100.qualifier
+Bundle-Version: 3.6.0.qualifier
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Require-Bundle: org.eclipse.equinox.preferences;bundle-version="[3.2.0,4.0.0)",
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentType.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentType.java
index 98057c3..23adf17 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentType.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentType.java
@@ -10,7 +10,6 @@
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
-import org.eclipse.core.runtime.QualifiedName;
 import java.io.*;
 import java.util.*;
 import org.eclipse.core.internal.runtime.RuntimeLog;
@@ -52,6 +51,10 @@
 	public final static String PREF_DEFAULT_CHARSET = "charset"; //$NON-NLS-1$
 	public final static String PREF_FILE_EXTENSIONS = "file-extensions"; //$NON-NLS-1$
 	public final static String PREF_FILE_NAMES = "file-names"; //$NON-NLS-1$
+	/**
+	 * @since 3.6
+	 */
+	public static final String PREF_USER_DEFINED = "userDefined"; //$NON-NLS-1$
 	final static byte PRIORITY_HIGH = 1;
 	final static byte PRIORITY_LOW = -1;
 	final static byte PRIORITY_NORMAL = 0;
@@ -73,6 +76,7 @@
 	String id;
 	private ContentTypeManager manager;
 	private String name;
+	private boolean userDefined;
 	private byte priority;
 	private ContentType target;
 	private String userCharset;
@@ -609,4 +613,9 @@
 		this.baseType = baseType;
 	}
 
+	@Override
+	public boolean isUserDefined() {
+		return this.userDefined;
+	}
+
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeBuilder.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeBuilder.java
index f4793fa..3b455c3 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeBuilder.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeBuilder.java
@@ -10,15 +10,12 @@
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
-import org.eclipse.core.runtime.QualifiedName;
-
 import java.util.*;
 import org.eclipse.core.internal.runtime.RuntimeLog;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.IContentDescription;
 import org.eclipse.core.runtime.content.IContentType;
-import org.eclipse.core.runtime.preferences.IEclipsePreferences;
-import org.eclipse.core.runtime.preferences.IPreferenceNodeVisitor;
+import org.eclipse.core.runtime.preferences.*;
 import org.eclipse.osgi.util.NLS;
 import org.osgi.service.prefs.BackingStoreException;
 
@@ -85,7 +82,7 @@
 	/**
 	 * Builds all content types found in the extension registry.
 	 */
-	public void buildCatalog() {
+	public void buildCatalog(IScopeContext context) {
 		IConfigurationElement[] allContentTypeCEs = getConfigurationElements();
 		for (int i = 0; i < allContentTypeCEs.length; i++)
 			if (allContentTypeCEs[i].getName().equals("content-type")) //$NON-NLS-1$
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeCatalog.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeCatalog.java
index 1153d7b..26aa2f4 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeCatalog.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeCatalog.java
@@ -10,10 +10,6 @@
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
-import java.util.Iterator;
-
-import java.util.Set;
-import org.eclipse.core.runtime.content.IContentType;
 import java.io.*;
 import java.util.*;
 import org.eclipse.core.runtime.*;
@@ -607,4 +603,12 @@
 		}
 		return destination;
 	}
+
+	void removeContentType(IContentType contentType) throws CoreException {
+		if (contentType.getSettings(getManager().getContext()).isUserDefined()) {
+			throw new IllegalArgumentException("content type must be user-defined."); //$NON-NLS-1$
+		}
+		contentTypes.remove(contentType.getId());
+	}
+
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeHandler.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeHandler.java
index dcf3926..aa9e5cf 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeHandler.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeHandler.java
@@ -202,4 +202,13 @@
 		return id;
 	}
 
+	@Override
+	public boolean isUserDefined() {
+		ContentType target = getTarget();
+		if (target != null) {
+			return target.isUserDefined();
+		}
+		return false;
+	}
+
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeManager.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeManager.java
index 8f67515..fee8a47 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeManager.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeManager.java
@@ -13,9 +13,14 @@
 
 import java.io.InputStream;
 import java.io.Reader;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.*;
 import org.eclipse.core.runtime.preferences.*;
+import org.eclipse.osgi.util.NLS;
+import org.osgi.service.prefs.BackingStoreException;
 
 public class ContentTypeManager extends ContentTypeMatcher implements IContentTypeManager {
 	private static ContentTypeManager instance;
@@ -140,7 +145,7 @@
 		// build catalog by parsing the extension registry
 		ContentTypeBuilder builder = createBuilder(newCatalog);
 		try {
-			builder.buildCatalog();
+			builder.buildCatalog(getContext());
 			// only remember catalog if building it was successful
 			catalog = newCatalog;
 		} catch (InvalidRegistryObjectException e) {
@@ -213,4 +218,60 @@
 		// this is the platform content type manager, no specificities
 		return description;
 	}
+
+	@Override
+	public void removeContentType(IContentType contentType) throws CoreException {
+		if (contentType.getSettings(getContext()).isUserDefined()) {
+			throw new IllegalArgumentException("content type must be user-defined."); //$NON-NLS-1$
+		}
+		if (!contentType.isUserDefined()) {
+			throw new IllegalArgumentException("Can only delete content-types defined by users."); //$NON-NLS-1$
+		}
+		getCatalog().removeContentType(contentType);
+		// remove preferences for this content type
+		String currentUserDefined = getContext().getNode(ContentType.PREF_USER_DEFINED)
+				.get(ContentType.PREF_USER_DEFINED, "");//$NON-NLS-1$
+		List<String> userDefinedIds = Arrays.asList(currentUserDefined.split(",")); //$NON-NLS-1$
+		userDefinedIds.remove(contentType.getId());
+		getContext().getNode(ContentType.PREF_USER_DEFINED).put(ContentType.PREF_USER_DEFINED,
+				userDefinedIds.stream().collect(Collectors.joining(","))); //$NON-NLS-1$
+		try {
+			getContext().getNode(ContentType.PREF_USER_DEFINED).flush();
+		} catch (BackingStoreException bse) {
+			String message = NLS.bind(ContentMessages.content_errorSavingSettings, contentType.getId());
+			IStatus status = new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, bse);
+			throw new CoreException(status);
+		}
+	}
+
+	@Override
+	public IContentType addContentType(String id, String name, IContentType baseType) throws CoreException {
+		if (id == null) {
+			throw new IllegalArgumentException("content-type 'id' mustn't be null");//$NON-NLS-1$
+		}
+		if (id.contains(",")) { //$NON-NLS-1$
+			throw new IllegalAccessError("Content-Type id mustn't contain ','"); //$NON-NLS-1$
+		}
+		if (getContentType(id) != null) {
+			throw new IllegalArgumentException("content-type '" + id + "' already exists.");//$NON-NLS-1$ //$NON-NLS-2$
+		}
+		ContentType contentType = ContentType.createContentType(getCatalog(), id, name, (byte) 0, new String[0],
+				new String[0], baseType.getId(), null, null, null);
+		getCatalog().addContentType(contentType);
+		// add preferences for this content type
+		String currentUserDefined = getContext().getNode(ContentType.PREF_USER_DEFINED)
+				.get(ContentType.PREF_USER_DEFINED, "");//$NON-NLS-1$
+		if (currentUserDefined.length() > 0) {
+			currentUserDefined += ",";//$NON-NLS-1$
+		}
+		getContext().getNode(ContentType.PREF_USER_DEFINED).put(ContentType.PREF_USER_DEFINED, currentUserDefined + id);
+		try {
+			getContext().getNode(ContentType.PREF_USER_DEFINED).flush();
+		} catch (BackingStoreException bse) {
+			String message = NLS.bind(ContentMessages.content_errorSavingSettings, id);
+			IStatus status = new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, bse);
+			throw new CoreException(status);
+		}
+		return contentType;
+	}
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeSettings.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeSettings.java
index 71387ee..60512f4 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeSettings.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeSettings.java
@@ -10,6 +10,7 @@
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
+import java.util.Arrays;
 import java.util.List;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.IContentDescription;
@@ -161,4 +162,12 @@
 		}
 	}
 
+	@Override
+	public boolean isUserDefined() {
+		return Arrays.asList(
+				context.getNode(ContentType.PREF_USER_DEFINED).get(ContentType.PREF_USER_DEFINED, "") //$NON-NLS-1$
+						.split(",")) //$NON-NLS-1$
+				.contains(contentType.getId());
+	}
+
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeManager.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeManager.java
index 1c9fb05..6bf307e 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeManager.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeManager.java
@@ -11,6 +11,7 @@
 package org.eclipse.core.runtime.content;
 
 import java.util.EventObject;
+import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.preferences.IScopeContext;
 
 /**
@@ -22,6 +23,7 @@
  *
  * @see org.eclipse.core.runtime.content.IContentTypeMatcher
  * @since 3.0
+ * @noimplement This interface is not intended to be implemented by clients.
  */
 public interface IContentTypeManager extends IContentTypeMatcher {
 
@@ -215,4 +217,32 @@
 	 * @see IContentTypeManager.IContentTypeChangeListener
 	 */
 	public void removeContentTypeChangeListener(IContentTypeChangeListener listener);
+
+	/**
+	 * Removes the content-type from internal registry.
+	 *
+	 * @param id
+	 *            the non-null content-type id
+	 * @param name
+	 *            the non-null user readable name
+	 * @param baseType
+	 *            parent base type. May be null.
+	 * @return the newly created and registered content-type
+	 * @throws IllegalArgumentException
+	 *             if one of the non-null arguments is null, or id is already
+	 *             used by another content-type
+	 *
+	 *             TODO move to a IContentTypeManagerExtension interface
+	 */
+	public IContentType addContentType(String id, String name, IContentType baseType) throws CoreException;
+
+	/**
+	 * Remove a content-type from underlying registry.
+	 *
+	 * @param contentType
+	 *            the content-type to remove.
+	 *
+	 *            TODO move to a IContentTypeManagerExtension interface
+	 */
+	public void removeContentType(IContentType contentType) throws CoreException;
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeSettings.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeSettings.java
index 17f9035..25ee261 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeSettings.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentTypeSettings.java
@@ -22,6 +22,7 @@
  * @see IContentType
  * @see IContentType#getSettings(IScopeContext)
  * @since 3.1
+ * @noimplement This interface is not intended to be implemented by clients.
  */
 public interface IContentTypeSettings {
 	/**
@@ -115,4 +116,10 @@
 	 * </ul>
 	 */
 	public void setDefaultCharset(String userCharset) throws CoreException;
+
+	/**
+	 * @return whether the content-type was defined by user
+	 * @since 3.6
+	 */
+	public boolean isUserDefined();
 }