Bug 263316 - Filename patterns for content types

Change-Id: I6e890f5b284e48b204a1c4fe860e64d88f25a641
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.core.contenttype/.settings/.api_filters b/bundles/org.eclipse.core.contenttype/.settings/.api_filters
index 8562598..97dfc21 100644
--- a/bundles/org.eclipse.core.contenttype/.settings/.api_filters
+++ b/bundles/org.eclipse.core.contenttype/.settings/.api_filters
@@ -1,5 +1,12 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.core.contenttype" version="2">
+    <resource path="src/org/eclipse/core/runtime/content/IContentType.java" type="org.eclipse.core.runtime.content.IContentType">
+        <filter comment="The Javadoc comment was replaced by proper API Tools tag" id="403853384">
+            <message_arguments>
+                <message_argument value="org.eclipse.core.runtime.content.IContentType"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/core/runtime/content/IContentTypeManager.java" type="org.eclipse.core.runtime.content.IContentTypeManager">
         <filter comment="https://bugs.eclipse.org/bugs/show_bug.cgi?id=57908#c37" id="403853384">
             <message_arguments>
@@ -7,11 +14,4 @@
             </message_arguments>
         </filter>
     </resource>
-    <resource path="src/org/eclipse/core/runtime/content/IContentTypeSettings.java" type="org.eclipse.core.runtime.content.IContentTypeSettings">
-        <filter comment="https://bugs.eclipse.org/bugs/show_bug.cgi?id=57908#c37" id="403853384">
-            <message_arguments>
-                <message_argument value="org.eclipse.core.runtime.content.IContentTypeSettings"/>
-            </message_arguments>
-        </filter>
-    </resource>
 </component>
diff --git a/bundles/org.eclipse.core.contenttype/META-INF/MANIFEST.MF b/bundles/org.eclipse.core.contenttype/META-INF/MANIFEST.MF
index 039b12b..c54f4d4 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.6.100.qualifier
+Bundle-Version: 3.7.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/pom.xml b/bundles/org.eclipse.core.contenttype/pom.xml
index 1fa77a2..bb2c76a 100644
--- a/bundles/org.eclipse.core.contenttype/pom.xml
+++ b/bundles/org.eclipse.core.contenttype/pom.xml
@@ -19,6 +19,6 @@
   </parent>
   <groupId>org.eclipse.core</groupId>
   <artifactId>org.eclipse.core.contenttype</artifactId>
-  <version>3.6.100-SNAPSHOT</version>
+  <version>3.7.0-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 </project>
diff --git a/bundles/org.eclipse.core.contenttype/schema/contentTypes.exsd b/bundles/org.eclipse.core.contenttype/schema/contentTypes.exsd
index 85a0b3d..59d5c30 100644
--- a/bundles/org.eclipse.core.contenttype/schema/contentTypes.exsd
+++ b/bundles/org.eclipse.core.contenttype/schema/contentTypes.exsd
@@ -2,9 +2,9 @@
 <!-- Schema file written by PDE -->
 <schema targetNamespace="org.eclipse.core.contenttype" xmlns="http://www.w3.org/2001/XMLSchema">
 <annotation>
-      <appinfo>
+      <appInfo>
          <meta.schema plugin="org.eclipse.core.contenttype" id="contentTypes" name="Content Types"/>
-      </appinfo>
+      </appInfo>
       <documentation>
          The content types extension point allows plug-ins to contribute to the platform content type catalog. There are two forms of contributions: &lt;cite&gt;content types&lt;/cite&gt; and &lt;cite&gt;file associations&lt;/cite&gt;.
 &lt;ul&gt;
@@ -24,9 +24,9 @@
 
    <element name="extension">
       <annotation>
-         <appinfo>
+         <appInfo>
             <meta.element />
-         </appinfo>
+         </appInfo>
       </annotation>
       <complexType>
          <sequence>
@@ -52,9 +52,9 @@
                <documentation>
                   an optional name of the extension instance
                </documentation>
-               <appinfo>
+               <appInfo>
                   <meta.attribute translatable="true"/>
-               </appinfo>
+               </appInfo>
             </annotation>
          </attribute>
       </complexType>
@@ -85,9 +85,9 @@
                <documentation>
                   the human-readable name of this content type
                </documentation>
-               <appinfo>
+               <appInfo>
                   <meta.attribute translatable="true"/>
-               </appinfo>
+               </appInfo>
             </annotation>
          </attribute>
          <attribute name="file-extensions" type="string">
@@ -104,6 +104,13 @@
                </documentation>
             </annotation>
          </attribute>
+         <attribute name="file-patterns" type="string">
+            <annotation>
+               <documentation>
+                  a comma-separated list of file name patterns to be associated with this content type. Since 3.7
+               </documentation>
+            </annotation>
+         </attribute>
          <attribute name="priority" use="default" value="normal">
             <annotation>
                <documentation>
@@ -138,9 +145,9 @@
                <documentation>
                   the fully qualified name of a class that implements &lt;samp&gt;org.eclipse.core.runtime.content.IContentDescriber&lt;/samp&gt; or &lt;samp&gt;org.eclipse.core.runtime.content.ITextContentDescriber&lt;/samp&gt;, or an empty string, if this content type should not have a describer even if the parent has one
                </documentation>
-               <appinfo>
+               <appInfo>
                   <meta.attribute kind="java"/>
-               </appinfo>
+               </appInfo>
             </annotation>
          </attribute>
          <attribute name="alias-for" type="string">
@@ -168,9 +175,9 @@
                <documentation>
                   the fully qualified name of a class that implements &lt;samp&gt;org.eclipse.core.runtime.content.IContentDescriber&lt;/samp&gt; or &lt;samp&gt;org.eclipse.core.runtime.content.ITextContentDescriber&lt;/samp&gt;, or an empty string, if this content type should not have a describer even if the parent has one
                </documentation>
-               <appinfo>
+               <appInfo>
                   <meta.attribute kind="java" basedOn="org.eclipse.core.runtime.content.IContentDescriber"/>
-               </appinfo>
+               </appInfo>
             </annotation>
          </attribute>
          <attribute name="plugin" type="string">
@@ -190,6 +197,9 @@
                <documentation>
                   the fully qualified identifier for the content type this file association contributes to
                </documentation>
+               <appInfo>
+                  <meta.attribute kind="identifier" basedOn="org.eclipse.core.contenttype.contentTypes/content-type/@id"/>
+               </appInfo>
             </annotation>
          </attribute>
          <attribute name="file-names" type="string">
@@ -206,6 +216,13 @@
                </documentation>
             </annotation>
          </attribute>
+         <attribute name="file-patterns" type="string">
+            <annotation>
+               <documentation>
+                  a comma-separated list of file name patterns to be associated with this content type. Since 3.7
+               </documentation>
+            </annotation>
+         </attribute>
       </complexType>
    </element>
 
@@ -253,18 +270,18 @@
    </element>
 
    <annotation>
-      <appinfo>
+      <appInfo>
          <meta.section type="since"/>
-      </appinfo>
+      </appInfo>
       <documentation>
          3.2
       </documentation>
    </annotation>
 
    <annotation>
-      <appinfo>
+      <appInfo>
          <meta.section type="examples"/>
-      </appinfo>
+      </appInfo>
       <documentation>
          Following is an example of a XML-based content type declaration using &lt;code&gt;org.eclipse.core.runtime.content.XMLRootElementContentDescriber2&lt;/code&gt;, 
 a built-in describer: 
@@ -315,9 +332,9 @@
    </annotation>
 
    <annotation>
-      <appinfo>
+      <appInfo>
          <meta.section type="apiInfo"/>
-      </appinfo>
+      </appInfo>
       <documentation>
          The value of the class attribute in the describer element must represent an 
 implementor of 
@@ -328,9 +345,9 @@
    </annotation>
 
    <annotation>
-      <appinfo>
+      <appInfo>
          <meta.section type="implementation"/>
-      </appinfo>
+      </appInfo>
       <documentation>
          &lt;p&gt;The org.eclipse.core.contenttype plug-in provides the following content types:&lt;ul&gt;
 &lt;li&gt;org.eclipse.core.runtime.text&lt;/li&gt;
@@ -348,9 +365,9 @@
    </annotation>
 
    <annotation>
-      <appinfo>
+      <appInfo>
          <meta.section type="copyright"/>
-      </appinfo>
+      </appInfo>
       <documentation>
          Copyright (c) 2004, 2008 IBM Corporation and others.&lt;br&gt;
 All rights reserved. This program and the accompanying materials are made 
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 136feaa..66c615c 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
@@ -51,6 +51,8 @@
 	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.7 */
+	public final static String PREF_FILE_PATTERNS = "file-patterns"; //$NON-NLS-1$
 	/** @since 3.6 */
 	public static final String PREF_USER_DEFINED = "userDefined"; //$NON-NLS-1$
 	/** @since 3.6 */
@@ -89,20 +91,26 @@
 	// -1 means unknown
 	private byte depth = -1;
 
-	public static ContentType createContentType(ContentTypeCatalog catalog, String uniqueId, String name, byte priority, String[] fileExtensions, String[] fileNames, String baseTypeId, String aliasTargetId, Map<QualifiedName, String> defaultProperties, IConfigurationElement contentTypeElement) {
+	public static ContentType createContentType(ContentTypeCatalog catalog, String uniqueId, String name, byte priority,
+			String[] fileExtensions, String[] fileNames, String[] filePatterns, String baseTypeId, String aliasTargetId,
+			Map<QualifiedName, String> defaultProperties, IConfigurationElement contentTypeElement) {
 		ContentType contentType = new ContentType(catalog.getManager());
 		contentType.catalog = catalog;
 		contentType.defaultDescription = new DefaultDescription(contentType);
 		contentType.id = uniqueId;
 		contentType.name = name;
 		contentType.priority = priority;
-		if ((fileExtensions != null && fileExtensions.length > 0) || (fileNames != null && fileNames.length > 0)) {
+		if ((fileExtensions != null && fileExtensions.length > 0) || (fileNames != null && fileNames.length > 0)
+				|| (filePatterns != null && filePatterns.length > 0)) {
 			contentType.builtInAssociations = true;
-			contentType.fileSpecs = new ArrayList<>(fileExtensions.length + fileNames.length);
+			contentType.fileSpecs = new ArrayList<>(fileExtensions.length + fileNames.length + filePatterns.length);
 			for (String fileName : fileNames)
 				contentType.internalAddFileSpec(fileName, FILE_NAME_SPEC | SPEC_PRE_DEFINED);
 			for (String fileExtension : fileExtensions)
 				contentType.internalAddFileSpec(fileExtension, FILE_EXTENSION_SPEC | SPEC_PRE_DEFINED);
+			for (String fileExtension : filePatterns) {
+				contentType.internalAddFileSpec(fileExtension, FILE_PATTERN_SPEC | SPEC_PRE_DEFINED);
+			}
 		}
 		contentType.defaultProperties = defaultProperties;
 		contentType.contentTypeElement = contentTypeElement;
@@ -120,6 +128,8 @@
 			return PREF_FILE_EXTENSIONS;
 		if ((flags & FILE_NAME_SPEC) != 0)
 			return PREF_FILE_NAMES;
+		if ((flags & FILE_PATTERN_SPEC) != 0)
+			return PREF_FILE_PATTERNS;
 		throw new IllegalArgumentException("Unknown type: " + flags); //$NON-NLS-1$
 	}
 
@@ -139,7 +149,8 @@
 
 	@Override
 	public void addFileSpec(String fileSpec, int type) throws CoreException {
-		Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_SPEC, "Unknown type: " + type); //$NON-NLS-1$
+		Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_SPEC || type == FILE_PATTERN_SPEC,
+				"Unknown type: " + type); //$NON-NLS-1$
 		String[] userSet;
 		synchronized (this) {
 			if (!internalAddFileSpec(fileSpec, type | SPEC_USER_DEFINED))
@@ -377,8 +388,10 @@
 	/**
 	 * Returns whether this content type has the given file spec.
 	 *
-	 * @param text the file spec string
-	 * @param typeMask FILE_NAME_SPEC or FILE_EXTENSION_SPEC
+	 * @param text
+	 *            the file spec string
+	 * @param typeMask
+	 *            FILE_NAME_SPEC or FILE_EXTENSION_SPEC or FILE_REGEXP_SPEC
 	 * @param strict
 	 * @return true if this file spec has already been added, false otherwise
 	 */
@@ -542,11 +555,18 @@
 		String[] fileExtensions = Util.parseItems(userSetFileExtensions);
 		for (String fileExtension : fileExtensions)
 			internalAddFileSpec(fileExtension, FILE_EXTENSION_SPEC | SPEC_USER_DEFINED);
+		// user set file name regexp
+		String userSetFileRegexp = contentTypeNode.get(PREF_FILE_PATTERNS, null);
+		String[] fileRegexps = Util.parseItems(userSetFileRegexp);
+		for (String fileRegexp : fileRegexps) {
+			internalAddFileSpec(fileRegexp, FILE_PATTERN_SPEC | SPEC_USER_DEFINED);
+		}
 	}
 
 	@Override
 	public void removeFileSpec(String fileSpec, int type) throws CoreException {
-		Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_SPEC, "Unknown type: " + type); //$NON-NLS-1$
+		Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_SPEC || type == FILE_PATTERN_SPEC,
+				"Unknown type: " + type); //$NON-NLS-1$
 		synchronized (this) {
 			if (!internalRemoveFileSpec(fileSpec, type | SPEC_USER_DEFINED))
 				return;
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 2dfcb9f..4d688cb 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
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2009 IBM Corporation and others.
+ * Copyright (c) 2004, 2017 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
@@ -7,6 +7,7 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Mickael Istria (Red Hat Inc.) - [263316] regexp for file association
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
@@ -77,6 +78,9 @@
 		String[] fileExtensions = Util.parseItems(fileAssociationElement.getAttribute("file-extensions")); //$NON-NLS-1$
 		for (String fileExtension : fileExtensions)
 			target.internalAddFileSpec(fileExtension, IContentType.FILE_EXTENSION_SPEC | ContentType.SPEC_PRE_DEFINED);
+		String[] filePatterns = Util.parseItems(fileAssociationElement.getAttribute("file-patterns")); //$NON-NLS-1$
+		for (String filePattern : filePatterns)
+			target.internalAddFileSpec(filePattern, IContentType.FILE_PATTERN_SPEC | ContentType.SPEC_PRE_DEFINED);
 	}
 
 	/**
@@ -91,7 +95,8 @@
 			IEclipsePreferences node = context.getNode(id);
 			catalog.addContentType(ContentType.createContentType(catalog, id,
 					node.get(ContentType.PREF_USER_DEFINED__NAME, ContentType.EMPTY_STRING),
-					(byte) 0, new String[0], new String[0], node.get(ContentType.PREF_USER_DEFINED__BASE_TYPE_ID, null), null, Collections.emptyMap(),
+					(byte) 0, new String[0], new String[0], new String[0],
+					node.get(ContentType.PREF_USER_DEFINED__BASE_TYPE_ID, null), null, Collections.emptyMap(),
 					null));
 		}
 		for (IConfigurationElement allContentTypeCE : allContentTypeCEs)
@@ -145,6 +150,7 @@
 		byte priority = parsePriority(contentTypeCE.getAttribute("priority")); //$NON-NLS-1$ );
 		String[] fileNames = Util.parseItems(contentTypeCE.getAttribute("file-names")); //$NON-NLS-1$
 		String[] fileExtensions = Util.parseItems(contentTypeCE.getAttribute("file-extensions")); //$NON-NLS-1$
+		String[] filePatterns = Util.parseItems(contentTypeCE.getAttribute("file-patterns")); //$NON-NLS-1$
 		String baseTypeId = getUniqueId(namespace, contentTypeCE.getAttribute("base-type")); //$NON-NLS-1$
 		String aliasTargetTypeId = getUniqueId(namespace, contentTypeCE.getAttribute("alias-for")); //$NON-NLS-1$
 		IConfigurationElement[] propertyCEs = null;
@@ -174,8 +180,8 @@
 				defaultProperties = Collections.singletonMap(IContentDescription.CHARSET, defaultCharset);
 			else if (!defaultProperties.containsKey(IContentDescription.CHARSET))
 				defaultProperties.put(IContentDescription.CHARSET, defaultCharset);
-		return ContentType.createContentType(catalog, uniqueId, name, priority, fileExtensions, fileNames, baseTypeId,
-				aliasTargetTypeId, defaultProperties, contentTypeCE);
+		return ContentType.createContentType(catalog, uniqueId, name, priority, fileExtensions, fileNames, filePatterns,
+				baseTypeId, aliasTargetTypeId, defaultProperties, contentTypeCE);
 	}
 
 	// Store this around for performance
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 5f23e1f..5a53975 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
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2004, 2016 IBM Corporation and others.
+ * Copyright (c) 2004, 2017 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
@@ -7,11 +7,14 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Mickael Istria (Red Hat Inc.) - [263316] regexp for file association
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
 import java.io.*;
 import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.*;
 import org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy;
@@ -28,6 +31,9 @@
 	private final Map<String, IContentType> contentTypes = new HashMap<>();
 	private final Map<String, Set<ContentType>> fileExtensions = new HashMap<>();
 	private final Map<String, Set<ContentType>> fileNames = new HashMap<>();
+	private final Map<String, Pattern> compiledRegexps = new HashMap<>();
+	private final Map<Pattern, String> initialPatternForRegexp = new HashMap<>();
+	private final Map<Pattern, Set<ContentType>> fileRegexps = new HashMap<>();
 	private int generation;
 	private ContentTypeManager manager;
 
@@ -127,13 +133,23 @@
 	};
 
 	private static IContentType[] concat(IContentType[][] types) {
-		if (types[0].length == 0)
-			return types[1];
-		if (types[1].length == 0)
-			return types[0];
-		IContentType[] result = new IContentType[types[0].length + types[1].length];
-		System.arraycopy(types[0], 0, result, 0, types[0].length);
-		System.arraycopy(types[1], 0, result, types[0].length, types[1].length);
+		int size = 0;
+		IContentType[] nonEmptyOne = NO_CONTENT_TYPES;
+		for (IContentType[] array : types) {
+			size += array.length;
+			if (array.length > 0) {
+				nonEmptyOne = array;
+			}
+		}
+		if (nonEmptyOne.length == size) { // no other array has content
+			return nonEmptyOne;
+		}
+		IContentType[] result = new IContentType[size];
+		int currentIndex = 0;
+		for (IContentType[] array : types) {
+			System.arraycopy(array, 0, result, currentIndex, array.length);
+			currentIndex += array.length;
+		}
 		return result;
 	}
 
@@ -174,15 +190,40 @@
 		String[] builtInFileExtensions = contentType.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_EXTENSION_SPEC);
 		for (String builtInFileExtension : builtInFileExtensions)
 			associate(contentType, builtInFileExtension, IContentType.FILE_EXTENSION_SPEC);
+		String[] builtInFilePatterns = contentType
+				.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_PATTERN_SPEC);
+		for (String builtInFilePattern : builtInFilePatterns) {
+			associate(contentType, builtInFilePattern, IContentType.FILE_PATTERN_SPEC);
+		}
+	}
+
+	String toRegexp(String filePattern) {
+		return filePattern.replace(".", "\\.").replace('?', '.').replace("*", ".*"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
 	}
 
 	synchronized void associate(ContentType contentType, String text, int type) {
-		Map<String, Set<ContentType>> fileSpecMap = ((type & IContentType.FILE_NAME_SPEC) != 0) ? fileNames : fileExtensions;
-		String mappingKey = FileSpec.getMappingKeyFor(text);
-		Set<ContentType> existing = fileSpecMap.get(mappingKey);
-		if (existing == null)
-			fileSpecMap.put(mappingKey, existing = new HashSet<>());
-		existing.add(contentType);
+		Map<String, Set<ContentType>> fileSpecMap = null;
+		if ((type & IContentType.FILE_NAME_SPEC) != 0) {
+			fileSpecMap = fileNames;
+		} else if ((type & IContentType.FILE_EXTENSION_SPEC) != 0) {
+			fileSpecMap = fileExtensions;
+		}
+		if (fileSpecMap != null) {
+			String mappingKey = FileSpec.getMappingKeyFor(text);
+			Set<ContentType> existing = fileSpecMap.get(mappingKey);
+			if (existing == null)
+				fileSpecMap.put(mappingKey, existing = new HashSet<>());
+			existing.add(contentType);
+		} else if ((type & IContentType.FILE_PATTERN_SPEC) != 0) {
+			Pattern compiledPattern = compiledRegexps.get(text);
+			if (compiledPattern == null) {
+				compiledPattern = Pattern.compile(toRegexp(text));
+				compiledRegexps.put(text, compiledPattern);
+				initialPatternForRegexp.put(compiledPattern, text);
+				fileRegexps.put(compiledPattern, new HashSet<>());
+			}
+			fileRegexps.get(compiledPattern).add(contentType);
+		}
 	}
 
 	private int collectMatchingByContents(int valid, IContentType[] subset, List<ContentType> destination, ILazySource contents, Map<String, Object> properties) throws IOException {
@@ -410,15 +451,26 @@
 		final int appropriateFullName = appropriate.size();
 		final int validExtension = collectMatchingByContents(validFullName, subset[1], appropriate, buffer, properties) - validFullName;
 		final int appropriateExtension = appropriate.size() - appropriateFullName;
+		final int validPattern = collectMatchingByContents(validExtension, subset[2], appropriate, buffer, properties)
+				- validExtension;
+		final int appropriatePattern = appropriate.size() - appropriateFullName - appropriateExtension;
 		IContentType[] result = appropriate.toArray(new IContentType[appropriate.size()]);
 		if (validFullName > 1)
 			Arrays.sort(result, 0, validFullName, validPolicy);
 		if (validExtension > 1)
 			Arrays.sort(result, validFullName, validFullName + validExtension, validPolicy);
+		if (validPattern > 1) {
+			Arrays.sort(result, validFullName + validExtension, validFullName + validExtension + validPattern,
+					validPolicy);
+		}
 		if (appropriateFullName - validFullName > 1)
 			Arrays.sort(result, validFullName + validExtension, appropriateFullName + validExtension, indeterminatePolicy);
 		if (appropriateExtension - validExtension > 1)
-			Arrays.sort(result, appropriateFullName + validExtension, appropriate.size(), indeterminatePolicy);
+			Arrays.sort(result, appropriateFullName + validExtension, appropriate.size() - validPattern,
+					indeterminatePolicy);
+		if (appropriatePattern - validPattern > 1) {
+			Arrays.sort(result, appropriate.size() - validPattern, appropriate.size(), indeterminatePolicy);
+		}
 		return result;
 	}
 
@@ -427,8 +479,9 @@
 		final Comparator<IContentType> validPolicy;
 		Comparator<IContentType> indeterminatePolicy;
 		if (fileName == null) {
-			// we only have a single array, by need to provide a two-dimensional, 2-element array
-			subset = new IContentType[][] {getAllContentTypes(), NO_CONTENT_TYPES};
+			// we only have a single array, by need to provide a two-dimensional, 3-element
+			// array
+			subset = new IContentType[][] { getAllContentTypes(), NO_CONTENT_TYPES, NO_CONTENT_TYPES };
 			indeterminatePolicy = policyConstantGeneralIsBetter;
 			validPolicy = policyConstantSpecificIsBetter;
 		} else {
@@ -436,13 +489,13 @@
 			indeterminatePolicy = policyGeneralIsBetter;
 			validPolicy = policySpecificIsBetter;
 		}
-		int total = subset[0].length + subset[1].length;
+		int total = subset[0].length + subset[1].length + subset[2].length;
 		if (total == 0)
 			// don't do further work if subset is empty
 			return NO_CONTENT_TYPES;
 		if (!forceValidation && total == 1) {
 			// do not do validation if not forced and only one was found (caller will validate later)
-			IContentType[] found = subset[0].length == 1 ? subset[0] : subset[1];
+			IContentType[] found = subset[0].length == 1 ? subset[0] : (subset[1].length == 1 ? subset[1] : subset[2]);
 			// bug 100032 - ignore binary content type if contents are text
 			if (!buffer.isText())
 				// binary buffer, caller can call the describer with no risk
@@ -466,10 +519,11 @@
 	 */
 	synchronized private IContentType[][] internalFindContentTypesFor(ContentTypeMatcher matcher, final String fileName, Comparator<IContentType> sortingPolicy) {
 		IScopeContext context = matcher.getContext();
-		IContentType[][] result = {NO_CONTENT_TYPES, NO_CONTENT_TYPES};
+		IContentType[][] result = { NO_CONTENT_TYPES, NO_CONTENT_TYPES, NO_CONTENT_TYPES };
+
+		Set<ContentType> existing = new HashSet<>();
 
 		final Set<ContentType> allByFileName;
-
 		if (context.equals(manager.getContext()))
 			allByFileName = getDirectlyAssociated(fileName, IContentTypeSettings.FILE_NAME_SPEC);
 		else {
@@ -478,7 +532,11 @@
 		}
 		Set<ContentType> selectedByName = selectMatchingByName(context, allByFileName, Collections.emptySet(), fileName,
 				IContentType.FILE_NAME_SPEC);
+		existing.addAll(selectedByName);
 		result[0] = selectedByName.toArray(new IContentType[selectedByName.size()]);
+		if (result[0].length > 1)
+			Arrays.sort(result[0], sortingPolicy);
+
 		final String fileExtension = ContentTypeManager.getFileExtension(fileName);
 		if (fileExtension != null) {
 			final Set<ContentType> allByFileExtension;
@@ -489,16 +547,44 @@
 				allByFileExtension.addAll(matcher.getDirectlyAssociated(this, fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC));
 			}
 			Set<ContentType> selectedByExtension = selectMatchingByName(context, allByFileExtension, selectedByName, fileExtension, IContentType.FILE_EXTENSION_SPEC);
+			existing.addAll(selectedByExtension);
 			if (!selectedByExtension.isEmpty())
 				result[1] = selectedByExtension.toArray(new IContentType[selectedByExtension.size()]);
 		}
-		if (result[0].length > 1)
-			Arrays.sort(result[0], sortingPolicy);
 		if (result[1].length > 1)
 			Arrays.sort(result[1], sortingPolicy);
+
+		final Set<ContentType> allByFilePattern;
+		if (context.equals(manager.getContext()))
+			allByFilePattern = getMatchingRegexpAssociated(fileName, IContentTypeSettings.FILE_PATTERN_SPEC);
+		else {
+			allByFilePattern = new HashSet<>(getMatchingRegexpAssociated(fileName,
+					IContentTypeSettings.FILE_PATTERN_SPEC | IContentType.IGNORE_USER_DEFINED));
+			allByFilePattern
+					.addAll(matcher.getMatchingRegexpAssociated(this, fileName,
+							IContentTypeSettings.FILE_PATTERN_SPEC));
+		}
+		existing.addAll(allByFilePattern);
+		if (!allByFilePattern.isEmpty())
+			result[2] = allByFilePattern.toArray(new IContentType[allByFilePattern.size()]);
+
 		return result;
 	}
 
+	private Set<ContentType> getMatchingRegexpAssociated(String fileName, int typeMask) {
+		if ((typeMask & IContentType.FILE_PATTERN_SPEC) == 0) {
+			throw new IllegalArgumentException("This method requires FILE_PATTERN_SPEC."); //$NON-NLS-1$
+		}
+		Set<ContentType> res = new HashSet<>();
+		for (Entry<Pattern, Set<ContentType>> spec : this.fileRegexps.entrySet()) {
+			if (spec.getKey().matcher(fileName).matches()) {
+				res.addAll(filterOnDefinitionSource(initialPatternForRegexp.get(spec.getKey()), typeMask,
+						spec.getValue()));
+			}
+		}
+		return res;
+	}
+
 	/**
 	 * Returns content types directly associated with the given file spec.
 	 *
@@ -513,29 +599,50 @@
 	 * @return a set of content types
 	 */
 	private Set<ContentType> getDirectlyAssociated(String text, int typeMask) {
+		if ((typeMask & IContentType.FILE_PATTERN_SPEC) != 0) {
+			throw new IllegalArgumentException("This method don't allow FILE_REGEXP_SPEC."); //$NON-NLS-1$
+		}
 		Map<String, Set<ContentType>> associations = (typeMask & IContentTypeSettings.FILE_NAME_SPEC) != 0 ? fileNames : fileExtensions;
-		Set<ContentType> result = null;
-		if ((typeMask & (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED)) == 0)
-			// no restrictions, get everything
-			result = associations.get(FileSpec.getMappingKeyFor(text));
-		else {
-			// only those specs satisfying the type mask should be included
-			Set<ContentType> initialSet = associations.get(FileSpec.getMappingKeyFor(text));
-			if (initialSet != null && !initialSet.isEmpty()) {
-				// copy so we can modify
-				result = new HashSet<>(initialSet);
-				// invert the last two bits so it is easier to compare
-				typeMask ^= (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED);
-				for (Iterator<ContentType> i = result.iterator(); i.hasNext();) {
-					ContentType contentType = i.next();
-					if (!contentType.hasFileSpec(text, typeMask, true))
-						i.remove();
-				}
-			}
+		Set<ContentType> result = associations.get(FileSpec.getMappingKeyFor(text));
+		if ((typeMask & (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED)) != 0) {
+			result = filterOnDefinitionSource(text, typeMask, result);
 		}
 		return result == null ? Collections.EMPTY_SET : result;
 	}
 
+	/**
+	 * Filters a set of content-types on whether they have a mapping that matches
+	 * provided criteria.
+	 *
+	 * @param text
+	 *            file name, file extension or file regexp (depending on value of
+	 *            {@code typeMask}.
+	 * @param typeMask
+	 *            the type mask. Spec type, and definition source (pre-defined or
+	 *            user-defined) will be used
+	 * @param contentTypes
+	 *            content types to filter from (not modified during method
+	 *            execution)
+	 * @return set of filtered content-type
+	 */
+	private Set<ContentType> filterOnDefinitionSource(String text, int typeMask, Set<ContentType> contentTypes) {
+		if ((typeMask & (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED)) == 0) {
+			return contentTypes;
+		}
+		if (contentTypes != null && !contentTypes.isEmpty()) {
+			// copy so we can modify
+			contentTypes = new HashSet<>(contentTypes);
+			// invert the last two bits so it is easier to compare
+			typeMask ^= (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED);
+			for (Iterator<ContentType> i = contentTypes.iterator(); i.hasNext();) {
+				ContentType contentType = i.next();
+				if (!contentType.hasFileSpec(text, typeMask, true))
+					i.remove();
+			}
+		}
+		return contentTypes;
+	}
+
 	synchronized ContentType internalGetContentType(String contentTypeIdentifier) {
 		return (ContentType) contentTypes.get(contentTypeIdentifier);
 	}
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 5b2158a..ec0a5dd 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
@@ -265,7 +265,7 @@
 			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 != null ? baseType.getId() : null, null, null, null);
+				new String[0], new String[0], baseType != null ? baseType.getId() : null, null, null, null);
 		getCatalog().addContentType(contentType);
 		// Add preferences for this content type.
 		String currentUserDefined = getContext().getNode(ContentType.PREF_USER_DEFINED)
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeMatcher.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeMatcher.java
index 89ba952..6d26b35 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeMatcher.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/internal/content/ContentTypeMatcher.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2005, 2007 IBM Corporation and others.
+ * Copyright (c) 2005, 2017 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
@@ -7,14 +7,17 @@
  *
  * Contributors:
  *     IBM Corporation - initial API and implementation
+ *     Mickael Istria (Red Hat Inc.) - [263316] regexp for file association
  *******************************************************************************/
 package org.eclipse.core.internal.content;
 
 import java.io.*;
 import java.util.*;
+import java.util.regex.Pattern;
 import org.eclipse.core.runtime.QualifiedName;
 import org.eclipse.core.runtime.content.*;
-import org.eclipse.core.runtime.preferences.*;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IScopeContext;
 import org.osgi.service.prefs.BackingStoreException;
 
 /**
@@ -93,26 +96,52 @@
 	 * Enumerates all content types whose settings satisfy the given file spec type mask.
 	 */
 	public Collection<ContentType> getDirectlyAssociated(final ContentTypeCatalog catalog, final String fileSpec, final int typeMask) {
+		if ((typeMask & (IContentType.FILE_EXTENSION_SPEC | IContentType.FILE_NAME_SPEC)) == 0) {
+			throw new IllegalArgumentException("This method only apply to name or extension based associations"); //$NON-NLS-1$
+		}
 		//TODO: make sure we include built-in associations as well
 		final IEclipsePreferences root = context.getNode(ContentTypeManager.CONTENT_TYPE_PREF_NODE);
 		final Set<ContentType> result = new HashSet<>(3);
 		try {
-			root.accept(new IPreferenceNodeVisitor() {
-				@Override
-				public boolean visit(IEclipsePreferences node) {
-					if (node == root)
-						return true;
-					String[] fileSpecs = ContentTypeSettings.getFileSpecs(node, typeMask);
-					for (String fileSpecification : fileSpecs)
-						if (fileSpecification.equalsIgnoreCase(fileSpec)) {
-							ContentType associated = catalog.getContentType(node.name());
-							if (associated != null)
-								result.add(associated);
-							break;
-						}
-					return false;
-				}
+			root.accept(node -> {
+				if (node == root)
+					return true;
+				String[] fileSpecs = ContentTypeSettings.getFileSpecs(node, typeMask);
+				for (String fileSpecification : fileSpecs)
+					if (fileSpecification.equalsIgnoreCase(fileSpec)) {
+						ContentType associated = catalog.getContentType(node.name());
+						if (associated != null)
+							result.add(associated);
+						break;
+					}
+				return false;
+			});
+		} catch (BackingStoreException bse) {
+			ContentType.log(ContentMessages.content_errorLoadingSettings, bse);
+		}
+		return result == null ? Collections.EMPTY_SET : result;
+	}
 
+	public Collection<? extends ContentType> getMatchingRegexpAssociated(ContentTypeCatalog catalog,
+			String fileName, final int typeMask) {
+		if ((typeMask & IContentType.FILE_PATTERN_SPEC) == 0) {
+			throw new IllegalArgumentException("This method only applies for FILE_REGEXP_SPEC."); //$NON-NLS-1$
+		}
+		final IEclipsePreferences root = context.getNode(ContentTypeManager.CONTENT_TYPE_PREF_NODE);
+		final Set<ContentType> result = new HashSet<>(3);
+		try {
+			root.accept(node -> {
+				if (node == root)
+					return true;
+				String[] fileSpecs = ContentTypeSettings.getFileSpecs(node, typeMask);
+				for (String fileSpecification : fileSpecs)
+					if (Pattern.matches(catalog.toRegexp(fileSpecification), fileName)) {
+						ContentType associated = catalog.getContentType(node.name());
+						if (associated != null)
+							result.add(associated);
+						break;
+					}
+				return false;
 			});
 		} catch (BackingStoreException bse) {
 			ContentType.log(ContentMessages.content_errorLoadingSettings, bse);
@@ -133,4 +162,5 @@
 		((ContentDescription) description).setContentTypeInfo(new ContentTypeSettings((ContentType) description.getContentTypeInfo(), context));
 		return description;
 	}
+
 }
diff --git a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentType.java b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentType.java
index 4fd5d2f..953ea0c 100644
--- a/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentType.java
+++ b/bundles/org.eclipse.core.contenttype/src/org/eclipse/core/runtime/content/IContentType.java
@@ -18,10 +18,8 @@
 /**
  * Content types represent and provide information on file types, such as
  * associated file names/extensions, default charset, etc.
- * <p>
- * This interface is not intended to be implemented by clients.
- * </p>
  *
+ * @noimplement This interface is not intended to be implemented by clients.
  * @since 3.0
  */
 public interface IContentType extends IContentTypeSettings {
@@ -29,20 +27,28 @@
 	 * File spec type flag constant, indicating that predefined file
 	 * specifications should not be taken into account.
 	 */
-	public static final int IGNORE_PRE_DEFINED = 0x01;
+	public static final int IGNORE_PRE_DEFINED = 0b1;
 	/**
 	 * File spec type flag constant, indicating that user-defined file
 	 * specifications should not be taken into account.
 	 */
-	public static final int IGNORE_USER_DEFINED = 0x02;
+	public static final int IGNORE_USER_DEFINED = 0b10;
 	/**
 	 * File spec type constant, indicating a file name specification.
 	 */
-	public static final int FILE_NAME_SPEC = 0x04;
+	public static final int FILE_NAME_SPEC = 0b100;
 	/**
 	 * File spec type constant, indicating a file extension specification.
 	 */
-	public static final int FILE_EXTENSION_SPEC = 0x08;
+	public static final int FILE_EXTENSION_SPEC = 0b1000;
+	/**
+	 * File spec type constant, indicating a file name pattern specification.
+	 * <code>?</code> represents any single character, <code>*<code> represent any
+	 * string.
+	 *
+	 * @since 3.7
+	 */
+	public static final int FILE_PATTERN_SPEC = 0b10000;
 
 	/**
 	 * Returns a reference to this content type's base type. If this content type
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 a89f67d..8156e7a 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
@@ -28,11 +28,18 @@
 	/**
 	 * File spec type constant, indicating a file extension specification.
 	 */
-	public static final int FILE_EXTENSION_SPEC = 0x08;
+	public static final int FILE_EXTENSION_SPEC = 0b1000;
 	/**
 	 * File spec type constant, indicating a file name specification.
 	 */
-	public static final int FILE_NAME_SPEC = 0x04;
+	public static final int FILE_NAME_SPEC = 0b100;
+
+	/**
+	 * File spec type constant, indicating a file pattern specification
+	 * 
+	 * @since 3.7
+	 */
+	public static final int FILE_PATTERN_SPEC = 0b10000;
 
 	/**
 	 * Adds a user-defined file specification to the corresponding content type. Has no
@@ -41,7 +48,8 @@
 	 * @param fileSpec the file specification
 	 * @param type the type of the file specification. One of
 	 * <code>FILE_NAME_SPEC</code>,
-	 * <code>FILE_EXTENSION_SPEC</code>.
+	 * <code>FILE_EXTENSION_SPEC</code>,
+	 * <code>FILE_PATTERN_SPEC</code>.
 	 * @throws IllegalArgumentException if the type bit mask is
 	 * incorrect
 	 * @throws CoreException if this method fails. Reasons include:
@@ -50,6 +58,7 @@
 	 * </ul>
 	 * @see #FILE_NAME_SPEC
 	 * @see #FILE_EXTENSION_SPEC
+	 * @see #FILE_PATTERN_SPEC
 	 */
 	public void addFileSpec(String fileSpec, int type) throws CoreException;