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