1 /*******************************************************************************
2  * Copyright (c) 2004, 2017 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Mickael Istria (Red Hat Inc.) - [263316] regexp for file association
14  *******************************************************************************/
15 package org.eclipse.core.internal.content;
16 
17 import java.io.*;
18 import java.util.*;
19 import java.util.Map.Entry;
20 import java.util.regex.Pattern;
21 import org.eclipse.core.runtime.*;
22 import org.eclipse.core.runtime.content.*;
23 import org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy;
24 import org.eclipse.core.runtime.preferences.IScopeContext;
25 import org.eclipse.osgi.util.NLS;
26 
27 public final class ContentTypeCatalog {
28 	private static final IContentType[] NO_CONTENT_TYPES = new IContentType[0];
29 
30 	/**
31 	 * All fields are guarded by lock on "this"
32 	 */
33 	private final Map<ContentType, ContentType[]> allChildren = new HashMap<>();
34 	private final Map<String, IContentType> contentTypes = new HashMap<>();
35 	private final Map<String, Set<ContentType>> fileExtensions = new HashMap<>();
36 	private final Map<String, Set<ContentType>> fileNames = new HashMap<>();
37 	private final Map<String, Pattern> compiledRegexps = new HashMap<>();
38 	private final Map<Pattern, String> initialPatternForRegexp = new HashMap<>();
39 	private final Map<Pattern, Set<ContentType>> fileRegexps = new HashMap<>();
40 	private int generation;
41 	private ContentTypeManager manager;
42 
43 	/**
44 	 * A sorting policy where the more generic content type wins. Lexicographical comparison is done
45 	 * as a last resort when all other criteria fail.
46 	 */
47 	private final Comparator<IContentType> policyConstantGeneralIsBetter = (IContentType o1, IContentType o2) -> {
48 		ContentType type1 = (ContentType) o1;
49 		ContentType type2 = (ContentType) o2;
50 		// first criteria: depth - the lower, the better
51 		int depthCriteria = type1.getDepth() - type2.getDepth();
52 		if (depthCriteria != 0)
53 			return depthCriteria;
54 		// second criteria: priority - the higher, the better
55 		int priorityCriteria = type1.getPriority() - type2.getPriority();
56 		if (priorityCriteria != 0)
57 			return -priorityCriteria;
58 		// they have same depth and priority - choose one arbitrarily (stability is important)
59 		return type1.getId().compareTo(type2.getId());
60 	};
61 
62 	/**
63 	 * A sorting policy where the more specific content type wins. Lexicographical comparison is done
64 	 * as a last resort when all other criteria fail.
65 	 */
66 	private Comparator<IContentType> policyConstantSpecificIsBetter = (IContentType o1, IContentType o2) -> {
67 		ContentType type1 = (ContentType) o1;
68 		ContentType type2 = (ContentType) o2;
69 		// first criteria: depth - the higher, the better
70 		int depthCriteria = type1.getDepth() - type2.getDepth();
71 		if (depthCriteria != 0)
72 			return -depthCriteria;
73 		// second criteria: priority - the higher, the better
74 		int priorityCriteria = type1.getPriority() - type2.getPriority();
75 		if (priorityCriteria != 0)
76 			return -priorityCriteria;
77 		// they have same depth and priority - choose one arbitrarily (stability is important)
78 		return type1.getId().compareTo(type2.getId());
79 	};
80 
81 	/**
82 	 * A sorting policy where the more general content type wins.
83 	 */
84 	private Comparator<IContentType> policyGeneralIsBetter = (IContentType o1, IContentType o2) -> {
85 		ContentType type1 = (ContentType) o1;
86 		ContentType type2 = (ContentType) o2;
87 		// first criteria: depth - the lower, the better
88 		int depthCriteria = type1.getDepth() - type2.getDepth();
89 		if (depthCriteria != 0)
90 			return depthCriteria;
91 		// second criteria: priority - the higher, the better
92 		int priorityCriteria = type1.getPriority() - type2.getPriority();
93 		if (priorityCriteria != 0)
94 			return -priorityCriteria;
95 		return 0;
96 	};
97 
98 	/**
99 	 * A sorting policy where content types are sorted by id.
100 	 */
101 	private Comparator<IContentType> policyLexicographical = (IContentType o1, IContentType o2) -> {
102 		ContentType type1 = (ContentType) o1;
103 		ContentType type2 = (ContentType) o2;
104 		return type1.getId().compareTo(type2.getId());
105 	};
106 	/**
107 	 * A sorting policy where the more specific content type wins.
108 	 */
109 	private Comparator<IContentType> policySpecificIsBetter = (IContentType o1, IContentType o2) -> {
110 		ContentType type1 = (ContentType) o1;
111 		ContentType type2 = (ContentType) o2;
112 		// first criteria: depth - the higher, the better
113 		int depthCriteria = type1.getDepth() - type2.getDepth();
114 		if (depthCriteria != 0)
115 			return -depthCriteria;
116 		// second criteria: priority - the higher, the better
117 		int priorityCriteria = type1.getPriority() - type2.getPriority();
118 		if (priorityCriteria != 0)
119 			return -priorityCriteria;
120 		return 0;
121 	};
122 
concat(IContentType[][] types)123 	private static IContentType[] concat(IContentType[][] types) {
124 		int size = 0;
125 		IContentType[] nonEmptyOne = NO_CONTENT_TYPES;
126 		for (IContentType[] array : types) {
127 			size += array.length;
128 			if (array.length > 0) {
129 				nonEmptyOne = array;
130 			}
131 		}
132 		if (nonEmptyOne.length == size) { // no other array has content
133 			return nonEmptyOne;
134 		}
135 		IContentType[] result = new IContentType[size];
136 		int currentIndex = 0;
137 		for (IContentType[] array : types) {
138 			System.arraycopy(array, 0, result, currentIndex, array.length);
139 			currentIndex += array.length;
140 		}
141 		return result;
142 	}
143 
ContentTypeCatalog(ContentTypeManager manager, int generation)144 	public ContentTypeCatalog(ContentTypeManager manager, int generation) {
145 		this.manager = manager;
146 		this.generation = generation;
147 	}
148 
addContentType(IContentType contentType)149 	synchronized void addContentType(IContentType contentType) {
150 		contentTypes.put(contentType.getId(), contentType);
151 	}
152 
153 	/**
154 	 * Applies a client-provided selection policy.
155 	 */
applyPolicy(final IContentTypeManager.ISelectionPolicy policy, final IContentType[] candidates, final boolean fileName, final boolean contents)156 	private IContentType[] applyPolicy(final IContentTypeManager.ISelectionPolicy policy, final IContentType[] candidates, final boolean fileName, final boolean contents) {
157 		final IContentType[][] result = new IContentType[][] {candidates};
158 		SafeRunner.run(new ISafeRunnable() {
159 			@Override
160 			public void handleException(Throwable exception) {
161 				// already logged in SafeRunner#run()
162 				// default result is the original array
163 				// nothing to be done
164 			}
165 
166 			@Override
167 			public void run() throws Exception {
168 				result[0] = policy.select(candidates, fileName, contents);
169 			}
170 		});
171 		return result[0];
172 	}
173 
associate(ContentType contentType)174 	private void associate(ContentType contentType) {
175 		String[] builtInFileNames = contentType.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_NAME_SPEC);
176 		for (String builtInFileName : builtInFileNames)
177 			associate(contentType, builtInFileName, IContentType.FILE_NAME_SPEC);
178 		String[] builtInFileExtensions = contentType.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_EXTENSION_SPEC);
179 		for (String builtInFileExtension : builtInFileExtensions)
180 			associate(contentType, builtInFileExtension, IContentType.FILE_EXTENSION_SPEC);
181 		String[] builtInFilePatterns = contentType
182 				.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_PATTERN_SPEC);
183 		for (String builtInFilePattern : builtInFilePatterns) {
184 			associate(contentType, builtInFilePattern, IContentType.FILE_PATTERN_SPEC);
185 		}
186 	}
187 
toRegexp(String filePattern)188 	String toRegexp(String filePattern) {
189 		return filePattern.replace(".", "\\.").replace('?', '.').replace("*", ".*"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
190 	}
191 
associate(ContentType contentType, String text, int type)192 	synchronized void associate(ContentType contentType, String text, int type) {
193 		Map<String, Set<ContentType>> fileSpecMap = null;
194 		if ((type & IContentType.FILE_NAME_SPEC) != 0) {
195 			fileSpecMap = fileNames;
196 		} else if ((type & IContentType.FILE_EXTENSION_SPEC) != 0) {
197 			fileSpecMap = fileExtensions;
198 		}
199 		if (fileSpecMap != null) {
200 			String mappingKey = FileSpec.getMappingKeyFor(text);
201 			Set<ContentType> existing = fileSpecMap.get(mappingKey);
202 			if (existing == null)
203 				fileSpecMap.put(mappingKey, existing = new HashSet<>());
204 			existing.add(contentType);
205 		} else if ((type & IContentType.FILE_PATTERN_SPEC) != 0) {
206 			Pattern compiledPattern = compiledRegexps.get(text);
207 			if (compiledPattern == null) {
208 				compiledPattern = Pattern.compile(toRegexp(text));
209 				compiledRegexps.put(text, compiledPattern);
210 				initialPatternForRegexp.put(compiledPattern, text);
211 				fileRegexps.put(compiledPattern, new HashSet<>());
212 			}
213 			fileRegexps.get(compiledPattern).add(contentType);
214 		}
215 	}
216 
collectMatchingByContents(int valid, IContentType[] subset, List<ContentType> destination, ILazySource contents, Map<String, Object> properties)217 	private int collectMatchingByContents(int valid, IContentType[] subset, List<ContentType> destination, ILazySource contents, Map<String, Object> properties) throws IOException {
218 		for (IContentType element : subset) {
219 			ContentType current = (ContentType) element;
220 			IContentDescriber describer = current.getDescriber();
221 			int status = IContentDescriber.INDETERMINATE;
222 			if (describer != null) {
223 				if (contents.isText() && !(describer instanceof ITextContentDescriber))
224 					// for text streams we skip content types that do not provide text-based content describers
225 					continue;
226 				status = describe(current, contents, null, properties);
227 				if (status == IContentDescriber.INVALID)
228 					continue;
229 			}
230 			if (status == IContentDescriber.VALID)
231 				destination.add(valid++, current);
232 			else
233 				destination.add(current);
234 		}
235 		return valid;
236 	}
237 
238 	@SuppressWarnings("deprecation")
describe(ContentType type, ILazySource contents, ContentDescription description, Map<String, Object> properties)239 	int describe(ContentType type, ILazySource contents, ContentDescription description, Map<String, Object> properties) throws IOException {
240 		IContentDescriber describer = type.getDescriber();
241 		try {
242 			if (contents.isText()) {
243 				if (describer instanceof XMLRootElementContentDescriber2) {
244 					return ((XMLRootElementContentDescriber2) describer).describe((Reader) contents, description, properties);
245 				} else if (describer instanceof XMLRootElementContentDescriber) {
246 					return ((XMLRootElementContentDescriber) describer).describe((Reader) contents, description, properties);
247 				}
248 				return ((ITextContentDescriber) describer).describe((Reader) contents, description);
249 			} else {
250 				if (describer instanceof XMLRootElementContentDescriber2) {
251 					return ((XMLRootElementContentDescriber2) describer).describe((InputStream) contents, description, properties);
252 				} else if (describer instanceof XMLRootElementContentDescriber) {
253 					return ((XMLRootElementContentDescriber) describer).describe((InputStream) contents, description, properties);
254 				}
255 				return (describer).describe((InputStream) contents, description);
256 			}
257 		} catch (RuntimeException re) {
258 			// describer seems to be buggy. just disable it (logging the reason)
259 			type.invalidateDescriber(re);
260 		} catch (Error e) {
261 			// describer got some serious problem. disable it (logging the reason) and throw the error again
262 			type.invalidateDescriber(e);
263 			throw e;
264 		} catch (LowLevelIOException llioe) {
265 			// throw the actual exception
266 			throw llioe.getActualException();
267 		} catch (IOException ioe) {
268 			// bugs 67841/ 62443  - non-low level IOException should be "ignored"
269 			if (ContentTypeManager.DEBUGGING) {
270 				String message = NLS.bind(ContentMessages.content_errorReadingContents, type.getId());
271 				ContentType.log(message, ioe);
272 			}
273 			// we don't know what the describer would say if the exception didn't occur
274 			return IContentDescriber.INDETERMINATE;
275 		} finally {
276 			contents.rewind();
277 		}
278 		return IContentDescriber.INVALID;
279 	}
280 
dissociate(ContentType contentType, String text, int type)281 	synchronized void dissociate(ContentType contentType, String text, int type) {
282 		Map<String, Set<ContentType>> fileSpecMap = ((type & IContentType.FILE_NAME_SPEC) != 0) ? fileNames : fileExtensions;
283 		String mappingKey = FileSpec.getMappingKeyFor(text);
284 		Set<ContentType> existing = fileSpecMap.get(mappingKey);
285 		if (existing == null)
286 			return;
287 		existing.remove(contentType);
288 	}
289 
290 	/**
291 	 * A content type will be valid if:
292 	 * <ol>
293 	 * <li>it does not designate a base type, or</li>
294 	 * <li>it designates a base type that exists and is valid</li>
295 	 * </ol>
296 	 * <p>And</p>:
297 	 * <ol>
298 	 * <li>it does not designate an alias type, or</li>
299 	 * <li>it designates an alias type that does not exist, or</li>
300 	 * <li>it designates an alias type that exists and is valid</li>
301 	 * </ol>
302 	 */
ensureValid(ContentType type)303 	private boolean ensureValid(ContentType type) {
304 		if (type.getValidation() != ContentType.STATUS_UNKNOWN)
305 			// already processed
306 			return type.isValid();
307 		// set this type temporarily as invalid to prevent cycles
308 		// all types in a cycle would remain as invalid
309 		type.setValidation(ContentType.STATUS_INVALID);
310 		if (type.isAlias())
311 			// it is an alias, leave as invalid
312 			return false;
313 		// check base type
314 		ContentType baseType = null;
315 		if (type.getBaseTypeId() != null) {
316 			baseType = (ContentType) contentTypes.get(type.getBaseTypeId());
317 			if (baseType == null)
318 				// invalid: specified base type is not known
319 				return false;
320 			// base type exists, ensure it is valid
321 			baseType = baseType.getAliasTarget(true);
322 			ensureValid(baseType);
323 			if (baseType.getValidation() != ContentType.STATUS_VALID)
324 				// invalid: base type was invalid
325 				return false;
326 		}
327 		// valid: all conditions satisfied
328 		type.setValidation(ContentType.STATUS_VALID);
329 		type.setBaseType(baseType);
330 		return true;
331 	}
332 
findContentTypesFor(ContentTypeMatcher matcher, InputStream contents, String fileName)333 	IContentType[] findContentTypesFor(ContentTypeMatcher matcher, InputStream contents, String fileName) throws IOException {
334 		final ILazySource buffer = ContentTypeManager.readBuffer(contents);
335 		IContentType[] selected = internalFindContentTypesFor(matcher, buffer, fileName, true);
336 		// give the policy a chance to change the results
337 		ISelectionPolicy policy = matcher.getPolicy();
338 		if (policy != null)
339 			selected = applyPolicy(policy, selected, fileName != null, true);
340 		return selected;
341 	}
342 
findContentTypesFor(ContentTypeMatcher matcher, final String fileName)343 	IContentType[] findContentTypesFor(ContentTypeMatcher matcher, final String fileName) {
344 		IContentType[] selected = concat(internalFindContentTypesFor(matcher, fileName, policyConstantGeneralIsBetter));
345 		// give the policy a chance to change the results
346 		ISelectionPolicy policy = matcher.getPolicy();
347 		if (policy != null)
348 			selected = applyPolicy(policy, selected, true, false);
349 		return selected;
350 	}
351 
getAllContentTypes()352 	synchronized public IContentType[] getAllContentTypes() {
353 		List<ContentType> result = new ArrayList<>(contentTypes.size());
354 		for (IContentType iContentType : contentTypes.values()) {
355 			ContentType type = (ContentType) iContentType;
356 			if (type.isValid() && !type.isAlias())
357 				result.add(type);
358 		}
359 		return result.toArray(new IContentType[result.size()]);
360 	}
361 
getChildren(ContentType parent)362 	private ContentType[] getChildren(ContentType parent) {
363 		ContentType[] children = allChildren.get(parent);
364 		if (children != null)
365 			return children;
366 		List<ContentType> result = new ArrayList<>(5);
367 		for (IContentType iContentType : this.contentTypes.values()) {
368 			ContentType next = (ContentType) iContentType;
369 			if (next.getBaseType() == parent)
370 				result.add(next);
371 		}
372 		children = result.toArray(new ContentType[result.size()]);
373 		allChildren.put(parent, children);
374 		return children;
375 	}
376 
getContentType(String contentTypeIdentifier)377 	public ContentType getContentType(String contentTypeIdentifier) {
378 		ContentType type = internalGetContentType(contentTypeIdentifier);
379 		return (type != null && type.isValid() && !type.isAlias()) ? type : null;
380 	}
381 
getDescriptionFor(ContentTypeMatcher matcher, ILazySource contents, String fileName, QualifiedName[] options)382 	private IContentDescription getDescriptionFor(ContentTypeMatcher matcher, ILazySource contents, String fileName, QualifiedName[] options) throws IOException {
383 		IContentType[] selected = internalFindContentTypesFor(matcher, contents, fileName, false);
384 		if (selected.length == 0)
385 			return null;
386 		// give the policy a chance to change the results
387 		ISelectionPolicy policy = matcher.getPolicy();
388 		if (policy != null) {
389 			selected = applyPolicy(policy, selected, fileName != null, true);
390 			if (selected.length == 0)
391 				return null;
392 		}
393 		return matcher.getSpecificDescription(((ContentType) selected[0]).internalGetDescriptionFor(contents, options));
394 	}
395 
getDescriptionFor(ContentTypeMatcher matcher, InputStream contents, String fileName, QualifiedName[] options)396 	public IContentDescription getDescriptionFor(ContentTypeMatcher matcher, InputStream contents, String fileName, QualifiedName[] options) throws IOException {
397 		return getDescriptionFor(matcher, ContentTypeManager.readBuffer(contents), fileName, options);
398 	}
399 
getDescriptionFor(ContentTypeMatcher matcher, Reader contents, String fileName, QualifiedName[] options)400 	public IContentDescription getDescriptionFor(ContentTypeMatcher matcher, Reader contents, String fileName, QualifiedName[] options) throws IOException {
401 		return getDescriptionFor(matcher, ContentTypeManager.readBuffer(contents), fileName, options);
402 	}
403 
getGeneration()404 	public int getGeneration() {
405 		return generation;
406 	}
407 
getManager()408 	public ContentTypeManager getManager() {
409 		return manager;
410 	}
411 
internalAccept(ContentTypeVisitor visitor, ContentType root)412 	private boolean internalAccept(ContentTypeVisitor visitor, ContentType root) {
413 		if (!root.isValid() || root.isAlias())
414 			return true;
415 		int result = visitor.visit(root);
416 		switch (result) {
417 				// stop traversing the tree
418 			case ContentTypeVisitor.STOP :
419 				return false;
420 				// stop traversing this subtree
421 			case ContentTypeVisitor.RETURN :
422 				return true;
423 		}
424 		ContentType[] children = getChildren(root);
425 		if (children == null)
426 			// this content type has no sub-types - keep traversing the tree
427 			return true;
428 		for (ContentType c : children) {
429 			if (!internalAccept(visitor, c)) {
430 				// stop the traversal
431 				return false;
432 			}
433 		}
434 		return true;
435 	}
436 
internalFindContentTypesFor(ILazySource buffer, IContentType[][] subset, Comparator<IContentType> validPolicy, Comparator<IContentType> indeterminatePolicy)437 	private IContentType[] internalFindContentTypesFor(ILazySource buffer, IContentType[][] subset, Comparator<IContentType> validPolicy, Comparator<IContentType> indeterminatePolicy) throws IOException {
438 		Map<String, Object> properties = new HashMap<>();
439 		final List<ContentType> appropriate = new ArrayList<>(5);
440 		final int validFullName = collectMatchingByContents(0, subset[0], appropriate, buffer, properties);
441 		final int appropriateFullName = appropriate.size();
442 		final int validExtension = collectMatchingByContents(validFullName, subset[1], appropriate, buffer, properties) - validFullName;
443 		final int appropriateExtension = appropriate.size() - appropriateFullName;
444 		final int validPattern = collectMatchingByContents(validExtension, subset[2], appropriate, buffer, properties)
445 				- validExtension;
446 		final int appropriatePattern = appropriate.size() - appropriateFullName - appropriateExtension;
447 		IContentType[] result = appropriate.toArray(new IContentType[appropriate.size()]);
448 		if (validFullName > 1)
449 			Arrays.sort(result, 0, validFullName, validPolicy);
450 		if (validExtension > 1)
451 			Arrays.sort(result, validFullName, validFullName + validExtension, validPolicy);
452 		if (validPattern > 1) {
453 			Arrays.sort(result, validFullName + validExtension, validFullName + validExtension + validPattern,
454 					validPolicy);
455 		}
456 		if (appropriateFullName - validFullName > 1)
457 			Arrays.sort(result, validFullName + validExtension, appropriateFullName + validExtension, indeterminatePolicy);
458 		if (appropriateExtension - validExtension > 1)
459 			Arrays.sort(result, appropriateFullName + validExtension, appropriate.size() - validPattern,
460 					indeterminatePolicy);
461 		if (appropriatePattern - validPattern > 1) {
462 			Arrays.sort(result, appropriate.size() - validPattern, appropriate.size(), indeterminatePolicy);
463 		}
464 		return result;
465 	}
466 
internalFindContentTypesFor(ContentTypeMatcher matcher, ILazySource buffer, String fileName, boolean forceValidation)467 	private IContentType[] internalFindContentTypesFor(ContentTypeMatcher matcher, ILazySource buffer, String fileName, boolean forceValidation) throws IOException {
468 		final IContentType[][] subset;
469 		final Comparator<IContentType> validPolicy;
470 		Comparator<IContentType> indeterminatePolicy;
471 		if (fileName == null) {
472 			// we only have a single array, by need to provide a two-dimensional, 3-element
473 			// array
474 			subset = new IContentType[][] { getAllContentTypes(), NO_CONTENT_TYPES, NO_CONTENT_TYPES };
475 			indeterminatePolicy = policyConstantGeneralIsBetter;
476 			validPolicy = policyConstantSpecificIsBetter;
477 		} else {
478 			subset = internalFindContentTypesFor(matcher, fileName, policyLexicographical);
479 			indeterminatePolicy = policyGeneralIsBetter;
480 			validPolicy = policySpecificIsBetter;
481 		}
482 		int total = subset[0].length + subset[1].length + subset[2].length;
483 		if (total == 0)
484 			// don't do further work if subset is empty
485 			return NO_CONTENT_TYPES;
486 		if (!forceValidation && total == 1) {
487 			// do not do validation if not forced and only one was found (caller will validate later)
488 			IContentType[] found = subset[0].length == 1 ? subset[0] : (subset[1].length == 1 ? subset[1] : subset[2]);
489 			// bug 100032 - ignore binary content type if contents are text
490 			if (!buffer.isText())
491 				// binary buffer, caller can call the describer with no risk
492 				return found;
493 			// text buffer, need to check describer
494 			IContentDescriber describer = ((ContentType) found[0]).getDescriber();
495 			if (describer == null || describer instanceof ITextContentDescriber)
496 				// no describer or text describer, that is fine
497 				return found;
498 			// only eligible content type is binary and contents are text, ignore it
499 			return NO_CONTENT_TYPES;
500 		}
501 		return internalFindContentTypesFor(buffer, subset, validPolicy, indeterminatePolicy);
502 	}
503 
504 	/**
505 	 * This is the implementation for file name based content type matching.
506 	 *
507 	 * @return all matching content types in the preferred order
508 	 * @see IContentTypeManager#findContentTypesFor(String)
509 	 */
internalFindContentTypesFor(ContentTypeMatcher matcher, final String fileName, Comparator<IContentType> sortingPolicy)510 	synchronized private IContentType[][] internalFindContentTypesFor(ContentTypeMatcher matcher, final String fileName, Comparator<IContentType> sortingPolicy) {
511 		IScopeContext context = matcher.getContext();
512 		IContentType[][] result = { NO_CONTENT_TYPES, NO_CONTENT_TYPES, NO_CONTENT_TYPES };
513 
514 		Set<ContentType> existing = new HashSet<>();
515 
516 		final Set<ContentType> allByFileName;
517 		if (context.equals(manager.getContext()))
518 			allByFileName = getDirectlyAssociated(fileName, IContentTypeSettings.FILE_NAME_SPEC);
519 		else {
520 			allByFileName = new HashSet<>(getDirectlyAssociated(fileName, IContentTypeSettings.FILE_NAME_SPEC | IContentType.IGNORE_USER_DEFINED));
521 			allByFileName.addAll(matcher.getDirectlyAssociated(this, fileName, IContentTypeSettings.FILE_NAME_SPEC));
522 		}
523 		Set<ContentType> selectedByName = selectMatchingByName(context, allByFileName, Collections.emptySet(), fileName,
524 				IContentType.FILE_NAME_SPEC);
525 		existing.addAll(selectedByName);
526 		result[0] = selectedByName.toArray(new IContentType[selectedByName.size()]);
527 		if (result[0].length > 1)
528 			Arrays.sort(result[0], sortingPolicy);
529 
530 		final String fileExtension = ContentTypeManager.getFileExtension(fileName);
531 		if (fileExtension != null) {
532 			final Set<ContentType> allByFileExtension;
533 			if (context.equals(manager.getContext()))
534 				allByFileExtension = getDirectlyAssociated(fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC);
535 			else {
536 				allByFileExtension = new HashSet<>(getDirectlyAssociated(fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC | IContentType.IGNORE_USER_DEFINED));
537 				allByFileExtension.addAll(matcher.getDirectlyAssociated(this, fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC));
538 			}
539 			Set<ContentType> selectedByExtension = selectMatchingByName(context, allByFileExtension, selectedByName, fileExtension, IContentType.FILE_EXTENSION_SPEC);
540 			existing.addAll(selectedByExtension);
541 			if (!selectedByExtension.isEmpty())
542 				result[1] = selectedByExtension.toArray(new IContentType[selectedByExtension.size()]);
543 		}
544 		if (result[1].length > 1)
545 			Arrays.sort(result[1], sortingPolicy);
546 
547 		final Set<ContentType> allByFilePattern;
548 		if (context.equals(manager.getContext()))
549 			allByFilePattern = getMatchingRegexpAssociated(fileName, IContentTypeSettings.FILE_PATTERN_SPEC);
550 		else {
551 			allByFilePattern = new HashSet<>(getMatchingRegexpAssociated(fileName,
552 					IContentTypeSettings.FILE_PATTERN_SPEC | IContentType.IGNORE_USER_DEFINED));
553 			allByFilePattern
554 					.addAll(matcher.getMatchingRegexpAssociated(this, fileName,
555 							IContentTypeSettings.FILE_PATTERN_SPEC));
556 		}
557 		existing.addAll(allByFilePattern);
558 		if (!allByFilePattern.isEmpty())
559 			result[2] = allByFilePattern.toArray(new IContentType[allByFilePattern.size()]);
560 
561 		return result;
562 	}
563 
getMatchingRegexpAssociated(String fileName, int typeMask)564 	private Set<ContentType> getMatchingRegexpAssociated(String fileName, int typeMask) {
565 		if ((typeMask & IContentType.FILE_PATTERN_SPEC) == 0) {
566 			throw new IllegalArgumentException("This method requires FILE_PATTERN_SPEC."); //$NON-NLS-1$
567 		}
568 		Set<ContentType> res = new HashSet<>();
569 		for (Entry<Pattern, Set<ContentType>> spec : this.fileRegexps.entrySet()) {
570 			if (spec.getKey().matcher(fileName).matches()) {
571 				res.addAll(filterOnDefinitionSource(initialPatternForRegexp.get(spec.getKey()), typeMask,
572 						spec.getValue()));
573 			}
574 		}
575 		return res;
576 	}
577 
578 	/**
579 	 * Returns content types directly associated with the given file spec.
580 	 *
581 	 * @param text a file name or extension
582 	 * @param typeMask a bit-wise or of the following flags:
583 	 * <ul>
584 	 * 		<li>IContentType.FILE_NAME, </li>
585 	 * 		<li>IContentType.FILE_EXTENSION, </li>
586 	 * 		<li>IContentType.IGNORE_PRE_DEFINED, </li>
587 	 * 		<li>IContentType.IGNORE_USER_DEFINED</li>
588 	 *	</ul>
589 	 * @return a set of content types
590 	 */
getDirectlyAssociated(String text, int typeMask)591 	private Set<ContentType> getDirectlyAssociated(String text, int typeMask) {
592 		if ((typeMask & IContentType.FILE_PATTERN_SPEC) != 0) {
593 			throw new IllegalArgumentException("This method don't allow FILE_REGEXP_SPEC."); //$NON-NLS-1$
594 		}
595 		Map<String, Set<ContentType>> associations = (typeMask & IContentTypeSettings.FILE_NAME_SPEC) != 0 ? fileNames : fileExtensions;
596 		Set<ContentType> result = associations.get(FileSpec.getMappingKeyFor(text));
597 		if ((typeMask & (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED)) != 0) {
598 			result = filterOnDefinitionSource(text, typeMask, result);
599 		}
600 		return result == null ? Collections.EMPTY_SET : result;
601 	}
602 
603 	/**
604 	 * Filters a set of content-types on whether they have a mapping that matches
605 	 * provided criteria.
606 	 *
607 	 * @param text
608 	 *            file name, file extension or file regexp (depending on value of
609 	 *            {@code typeMask}.
610 	 * @param typeMask
611 	 *            the type mask. Spec type, and definition source (pre-defined or
612 	 *            user-defined) will be used
613 	 * @param contentTypes
614 	 *            content types to filter from (not modified during method
615 	 *            execution)
616 	 * @return set of filtered content-type
617 	 */
filterOnDefinitionSource(String text, int typeMask, Set<ContentType> contentTypes)618 	private Set<ContentType> filterOnDefinitionSource(String text, int typeMask, Set<ContentType> contentTypes) {
619 		if ((typeMask & (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED)) == 0) {
620 			return contentTypes;
621 		}
622 		if (contentTypes != null && !contentTypes.isEmpty()) {
623 			// copy so we can modify
624 			contentTypes = new HashSet<>(contentTypes);
625 			// invert the last two bits so it is easier to compare
626 			typeMask ^= (IContentType.IGNORE_PRE_DEFINED | IContentType.IGNORE_USER_DEFINED);
627 			for (Iterator<ContentType> i = contentTypes.iterator(); i.hasNext();) {
628 				ContentType contentType = i.next();
629 				if (!contentType.hasFileSpec(text, typeMask, true))
630 					i.remove();
631 			}
632 		}
633 		return contentTypes;
634 	}
635 
internalGetContentType(String contentTypeIdentifier)636 	synchronized ContentType internalGetContentType(String contentTypeIdentifier) {
637 		return (ContentType) contentTypes.get(contentTypeIdentifier);
638 	}
639 
makeAliases()640 	private void makeAliases() {
641 		// process all content types marking aliases appropriately
642 		for (IContentType iContentType : contentTypes.values()) {
643 			ContentType type = (ContentType) iContentType;
644 			String targetId = type.getAliasTargetId();
645 			if (targetId == null)
646 				continue;
647 			ContentType target = internalGetContentType(targetId);
648 			if (target != null)
649 				type.setAliasTarget(target);
650 		}
651 	}
652 
653 	/**
654 	 * Resolves inter-content type associations (inheritance and aliasing).
655 	 */
organize()656 	synchronized protected void organize() {
657 		// build the aliasing
658 		makeAliases();
659 		// do the validation
660 		for (IContentType iContentType : contentTypes.values()) {
661 			ContentType type = (ContentType) iContentType;
662 			if (ensureValid(type))
663 				associate(type);
664 		}
665 		if (ContentTypeManager.DEBUGGING)
666 			for (IContentType iContentType : contentTypes.values()) {
667 				ContentType type = (ContentType) iContentType;
668 				if (!type.isValid())
669 					ContentMessages.message("Invalid: " + type); //$NON-NLS-1$
670 			}
671 	}
672 
673 	/**
674 	 * Processes all content types in source, adding those matching the given file spec to the
675 	 * destination collection.
676 	 */
selectMatchingByName(final IScopeContext context, Collection<ContentType> source, final Collection<ContentType> existing, final String fileSpecText, final int fileSpecType)677 	private Set<ContentType> selectMatchingByName(final IScopeContext context, Collection<ContentType> source, final Collection<ContentType> existing, final String fileSpecText, final int fileSpecType) {
678 		if (source == null || source.isEmpty())
679 			return Collections.EMPTY_SET;
680 		final Set<ContentType> destination = new HashSet<>(5);
681 		// process all content types in the given collection
682 		for (ContentType root : source) {
683 			// From a given content type, check if it matches, and
684 			// include any children that match as well.
685 			internalAccept(new ContentTypeVisitor() {
686 				@Override
687 				public int visit(ContentType type) {
688 					if (type != root && type.hasBuiltInAssociations())
689 						// this content type has built-in associations - visit it later as root
690 						return RETURN;
691 					if (type == root && !type.hasFileSpec(context, fileSpecText, fileSpecType))
692 						// it is the root and does not match the file name - do not add it nor look into its children
693 						return RETURN;
694 					// either the content type is the root and matches the file name or
695 					// is a sub content type and does not have built-in files specs
696 					if (!existing.contains(type))
697 						destination.add(type);
698 					return CONTINUE;
699 				}
700 			}, root);
701 		}
702 		return destination;
703 	}
704 
removeContentType(String contentTypeIdentifier)705 	void removeContentType(String contentTypeIdentifier) throws CoreException {
706 		ContentType contentType = getContentType(contentTypeIdentifier);
707 		if (contentType == null) {
708 			return;
709 		}
710 		if (!contentType.isUserDefined()) {
711 			throw new IllegalArgumentException("Content type must be user-defined."); //$NON-NLS-1$
712 		}
713 		contentTypes.remove(contentType.getId());
714 	}
715 
716 }
717