blob: 79b94199a9a688bdd734f5060a28a750c6fd5f5a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2016 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
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.core.internal.content;
import java.io.*;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.content.*;
import org.eclipse.core.runtime.content.IContentTypeManager.ISelectionPolicy;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.osgi.util.NLS;
public final class ContentTypeCatalog {
private static final IContentType[] NO_CONTENT_TYPES = new IContentType[0];
/**
* All fields are guarded by lock on "this"
*/
private final Map<ContentType, ContentType[]> allChildren = new HashMap<>();
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 int generation;
private ContentTypeManager manager;
/**
* A sorting policy where the more generic content type wins. Lexicographical comparison is done
* as a last resort when all other criteria fail.
*/
private final Comparator<IContentType> policyConstantGeneralIsBetter = new Comparator<IContentType>() {
@Override
public int compare(IContentType o1, IContentType o2) {
ContentType type1 = (ContentType) o1;
ContentType type2 = (ContentType) o2;
// first criteria: depth - the lower, the better
int depthCriteria = type1.getDepth() - type2.getDepth();
if (depthCriteria != 0)
return depthCriteria;
// second criteria: priority - the higher, the better
int priorityCriteria = type1.getPriority() - type2.getPriority();
if (priorityCriteria != 0)
return -priorityCriteria;
// they have same depth and priority - choose one arbitrarily (stability is important)
return type1.getId().compareTo(type2.getId());
}
};
/**
* A sorting policy where the more specific content type wins. Lexicographical comparison is done
* as a last resort when all other criteria fail.
*/
private Comparator<IContentType> policyConstantSpecificIsBetter = new Comparator<IContentType>() {
@Override
public int compare(IContentType o1, IContentType o2) {
ContentType type1 = (ContentType) o1;
ContentType type2 = (ContentType) o2;
// first criteria: depth - the higher, the better
int depthCriteria = type1.getDepth() - type2.getDepth();
if (depthCriteria != 0)
return -depthCriteria;
// second criteria: priority - the higher, the better
int priorityCriteria = type1.getPriority() - type2.getPriority();
if (priorityCriteria != 0)
return -priorityCriteria;
// they have same depth and priority - choose one arbitrarily (stability is important)
return type1.getId().compareTo(type2.getId());
}
};
/**
* A sorting policy where the more general content type wins.
*/
private Comparator<IContentType> policyGeneralIsBetter = new Comparator<IContentType>() {
@Override
public int compare(IContentType o1, IContentType o2) {
ContentType type1 = (ContentType) o1;
ContentType type2 = (ContentType) o2;
// first criteria: depth - the lower, the better
int depthCriteria = type1.getDepth() - type2.getDepth();
if (depthCriteria != 0)
return depthCriteria;
// second criteria: priority - the higher, the better
int priorityCriteria = type1.getPriority() - type2.getPriority();
if (priorityCriteria != 0)
return -priorityCriteria;
return 0;
}
};
/**
* A sorting policy where content types are sorted by id.
*/
private Comparator<IContentType> policyLexicographical = new Comparator<IContentType>() {
@Override
public int compare(IContentType o1, IContentType o2) {
ContentType type1 = (ContentType) o1;
ContentType type2 = (ContentType) o2;
return type1.getId().compareTo(type2.getId());
}
};
/**
* A sorting policy where the more specific content type wins.
*/
private Comparator<IContentType> policySpecificIsBetter = new Comparator<IContentType>() {
@Override
public int compare(IContentType o1, IContentType o2) {
ContentType type1 = (ContentType) o1;
ContentType type2 = (ContentType) o2;
// first criteria: depth - the higher, the better
int depthCriteria = type1.getDepth() - type2.getDepth();
if (depthCriteria != 0)
return -depthCriteria;
// second criteria: priority - the higher, the better
int priorityCriteria = type1.getPriority() - type2.getPriority();
if (priorityCriteria != 0)
return -priorityCriteria;
return 0;
}
};
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);
return result;
}
public ContentTypeCatalog(ContentTypeManager manager, int generation) {
this.manager = manager;
this.generation = generation;
}
synchronized void addContentType(IContentType contentType) {
contentTypes.put(contentType.getId(), contentType);
}
/**
* Applies a client-provided selection policy.
*/
private IContentType[] applyPolicy(final IContentTypeManager.ISelectionPolicy policy, final IContentType[] candidates, final boolean fileName, final boolean contents) {
final IContentType[][] result = new IContentType[][] {candidates};
SafeRunner.run(new ISafeRunnable() {
@Override
public void handleException(Throwable exception) {
// already logged in SafeRunner#run()
// default result is the original array
// nothing to be done
}
@Override
public void run() throws Exception {
result[0] = policy.select(candidates, fileName, contents);
}
});
return result[0];
}
private void associate(ContentType contentType) {
String[] builtInFileNames = contentType.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_NAME_SPEC);
for (int i = 0; i < builtInFileNames.length; i++)
associate(contentType, builtInFileNames[i], IContentType.FILE_NAME_SPEC);
String[] builtInFileExtensions = contentType.getFileSpecs(IContentType.IGNORE_USER_DEFINED | IContentType.FILE_EXTENSION_SPEC);
for (int i = 0; i < builtInFileExtensions.length; i++)
associate(contentType, builtInFileExtensions[i], IContentType.FILE_EXTENSION_SPEC);
}
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);
}
private int collectMatchingByContents(int valid, IContentType[] subset, List<ContentType> destination, ILazySource contents, Map<String, Object> properties) throws IOException {
for (int i = 0; i < subset.length; i++) {
ContentType current = (ContentType) subset[i];
IContentDescriber describer = current.getDescriber();
int status = IContentDescriber.INDETERMINATE;
if (describer != null) {
if (contents.isText() && !(describer instanceof ITextContentDescriber))
// for text streams we skip content types that do not provide text-based content describers
continue;
status = describe(current, contents, null, properties);
if (status == IContentDescriber.INVALID)
continue;
}
if (status == IContentDescriber.VALID)
destination.add(valid++, current);
else
destination.add(current);
}
return valid;
}
@SuppressWarnings("deprecation")
int describe(ContentType type, ILazySource contents, ContentDescription description, Map<String, Object> properties) throws IOException {
IContentDescriber describer = type.getDescriber();
try {
if (contents.isText()) {
if (describer instanceof XMLRootElementContentDescriber2) {
return ((XMLRootElementContentDescriber2) describer).describe((Reader) contents, description, properties);
} else if (describer instanceof XMLRootElementContentDescriber) {
return ((XMLRootElementContentDescriber) describer).describe((Reader) contents, description, properties);
}
return ((ITextContentDescriber) describer).describe((Reader) contents, description);
} else {
if (describer instanceof XMLRootElementContentDescriber2) {
return ((XMLRootElementContentDescriber2) describer).describe((InputStream) contents, description, properties);
} else if (describer instanceof XMLRootElementContentDescriber) {
return ((XMLRootElementContentDescriber) describer).describe((InputStream) contents, description, properties);
}
return (describer).describe((InputStream) contents, description);
}
} catch (RuntimeException re) {
// describer seems to be buggy. just disable it (logging the reason)
type.invalidateDescriber(re);
} catch (Error e) {
// describer got some serious problem. disable it (logging the reason) and throw the error again
type.invalidateDescriber(e);
throw e;
} catch (LowLevelIOException llioe) {
// throw the actual exception
throw llioe.getActualException();
} catch (IOException ioe) {
// bugs 67841/ 62443 - non-low level IOException should be "ignored"
if (ContentTypeManager.DEBUGGING) {
String message = NLS.bind(ContentMessages.content_errorReadingContents, type.getId());
ContentType.log(message, ioe);
}
// we don't know what the describer would say if the exception didn't occur
return IContentDescriber.INDETERMINATE;
} finally {
contents.rewind();
}
return IContentDescriber.INVALID;
}
synchronized void dissociate(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)
return;
existing.remove(contentType);
}
/**
* A content type will be valid if:
* <ol>
* <li>it does not designate a base type, or</li>
* <li>it designates a base type that exists and is valid</li>
* </ol>
* <p>And</p>:
* <ol>
* <li>it does not designate an alias type, or</li>
* <li>it designates an alias type that does not exist, or</li>
* <li>it designates an alias type that exists and is valid</li>
* </ol>
*/
private boolean ensureValid(ContentType type) {
if (type.getValidation() != ContentType.STATUS_UNKNOWN)
// already processed
return type.isValid();
// set this type temporarily as invalid to prevent cycles
// all types in a cycle would remain as invalid
type.setValidation(ContentType.STATUS_INVALID);
if (type.isAlias())
// it is an alias, leave as invalid
return false;
// check base type
ContentType baseType = null;
if (type.getBaseTypeId() != null) {
baseType = (ContentType) contentTypes.get(type.getBaseTypeId());
if (baseType == null)
// invalid: specified base type is not known
return false;
// base type exists, ensure it is valid
baseType = baseType.getAliasTarget(true);
ensureValid(baseType);
if (baseType.getValidation() != ContentType.STATUS_VALID)
// invalid: base type was invalid
return false;
}
// valid: all conditions satisfied
type.setValidation(ContentType.STATUS_VALID);
type.setBaseType(baseType);
return true;
}
IContentType[] findContentTypesFor(ContentTypeMatcher matcher, InputStream contents, String fileName) throws IOException {
final ILazySource buffer = ContentTypeManager.readBuffer(contents);
IContentType[] selected = internalFindContentTypesFor(matcher, buffer, fileName, true);
// give the policy a chance to change the results
ISelectionPolicy policy = matcher.getPolicy();
if (policy != null)
selected = applyPolicy(policy, selected, fileName != null, true);
return selected;
}
IContentType[] findContentTypesFor(ContentTypeMatcher matcher, final String fileName) {
IContentType[] selected = concat(internalFindContentTypesFor(matcher, fileName, policyConstantGeneralIsBetter));
// give the policy a chance to change the results
ISelectionPolicy policy = matcher.getPolicy();
if (policy != null)
selected = applyPolicy(policy, selected, true, false);
return selected;
}
synchronized public IContentType[] getAllContentTypes() {
List<ContentType> result = new ArrayList<>(contentTypes.size());
for (Iterator<IContentType> i = contentTypes.values().iterator(); i.hasNext();) {
ContentType type = (ContentType) i.next();
if (type.isValid() && !type.isAlias())
result.add(type);
}
return result.toArray(new IContentType[result.size()]);
}
private ContentType[] getChildren(ContentType parent) {
ContentType[] children = allChildren.get(parent);
if (children != null)
return children;
List<ContentType> result = new ArrayList<>(5);
for (Iterator<IContentType> i = this.contentTypes.values().iterator(); i.hasNext();) {
ContentType next = (ContentType) i.next();
if (next.getBaseType() == parent)
result.add(next);
}
children = result.toArray(new ContentType[result.size()]);
allChildren.put(parent, children);
return children;
}
public ContentType getContentType(String contentTypeIdentifier) {
ContentType type = internalGetContentType(contentTypeIdentifier);
return (type != null && type.isValid() && !type.isAlias()) ? type : null;
}
private IContentDescription getDescriptionFor(ContentTypeMatcher matcher, ILazySource contents, String fileName, QualifiedName[] options) throws IOException {
IContentType[] selected = internalFindContentTypesFor(matcher, contents, fileName, false);
if (selected.length == 0)
return null;
// give the policy a chance to change the results
ISelectionPolicy policy = matcher.getPolicy();
if (policy != null) {
selected = applyPolicy(policy, selected, fileName != null, true);
if (selected.length == 0)
return null;
}
return matcher.getSpecificDescription(((ContentType) selected[0]).internalGetDescriptionFor(contents, options));
}
public IContentDescription getDescriptionFor(ContentTypeMatcher matcher, InputStream contents, String fileName, QualifiedName[] options) throws IOException {
return getDescriptionFor(matcher, ContentTypeManager.readBuffer(contents), fileName, options);
}
public IContentDescription getDescriptionFor(ContentTypeMatcher matcher, Reader contents, String fileName, QualifiedName[] options) throws IOException {
return getDescriptionFor(matcher, ContentTypeManager.readBuffer(contents), fileName, options);
}
public int getGeneration() {
return generation;
}
public ContentTypeManager getManager() {
return manager;
}
private boolean internalAccept(ContentTypeVisitor visitor, ContentType root) {
if (!root.isValid() || root.isAlias())
return true;
int result = visitor.visit(root);
switch (result) {
// stop traversing the tree
case ContentTypeVisitor.STOP :
return false;
// stop traversing this subtree
case ContentTypeVisitor.RETURN :
return true;
}
ContentType[] children = getChildren(root);
if (children == null)
// this content type has no sub-types - keep traversing the tree
return true;
for (int i = 0; i < children.length; i++)
if (!internalAccept(visitor, children[i]))
// stop the traversal
return false;
return true;
}
private IContentType[] internalFindContentTypesFor(ILazySource buffer, IContentType[][] subset, Comparator<IContentType> validPolicy, Comparator<IContentType> indeterminatePolicy) throws IOException {
Map<String, Object> properties = new HashMap<>();
final List<ContentType> appropriate = new ArrayList<>(5);
final int validFullName = collectMatchingByContents(0, subset[0], appropriate, buffer, properties);
final int appropriateFullName = appropriate.size();
final int validExtension = collectMatchingByContents(validFullName, subset[1], appropriate, buffer, properties) - validFullName;
final int appropriateExtension = appropriate.size() - appropriateFullName;
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 (appropriateFullName - validFullName > 1)
Arrays.sort(result, validFullName + validExtension, appropriateFullName + validExtension, indeterminatePolicy);
if (appropriateExtension - validExtension > 1)
Arrays.sort(result, appropriateFullName + validExtension, appropriate.size(), indeterminatePolicy);
return result;
}
private IContentType[] internalFindContentTypesFor(ContentTypeMatcher matcher, ILazySource buffer, String fileName, boolean forceValidation) throws IOException {
final IContentType[][] subset;
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};
indeterminatePolicy = policyConstantGeneralIsBetter;
validPolicy = policyConstantSpecificIsBetter;
} else {
subset = internalFindContentTypesFor(matcher, fileName, policyLexicographical);
indeterminatePolicy = policyGeneralIsBetter;
validPolicy = policySpecificIsBetter;
}
int total = subset[0].length + subset[1].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];
// bug 100032 - ignore binary content type if contents are text
if (!buffer.isText())
// binary buffer, caller can call the describer with no risk
return found;
// text buffer, need to check describer
IContentDescriber describer = ((ContentType) found[0]).getDescriber();
if (describer == null || describer instanceof ITextContentDescriber)
// no describer or text describer, that is fine
return found;
// only eligible content type is binary and contents are text, ignore it
return NO_CONTENT_TYPES;
}
return internalFindContentTypesFor(buffer, subset, validPolicy, indeterminatePolicy);
}
/**
* This is the implementation for file name based content type matching.
*
* @return all matching content types in the preferred order
* @see IContentTypeManager#findContentTypesFor(String)
*/
synchronized private IContentType[][] internalFindContentTypesFor(ContentTypeMatcher matcher, final String fileName, Comparator<IContentType> sortingPolicy) {
IScopeContext context = matcher.getContext();
IContentType[][] result = {NO_CONTENT_TYPES, NO_CONTENT_TYPES};
final Set<ContentType> allByFileName;
if (context.equals(manager.getContext()))
allByFileName = getDirectlyAssociated(fileName, IContentTypeSettings.FILE_NAME_SPEC);
else {
allByFileName = new HashSet<>(getDirectlyAssociated(fileName, IContentTypeSettings.FILE_NAME_SPEC | IContentType.IGNORE_USER_DEFINED));
allByFileName.addAll(matcher.getDirectlyAssociated(this, fileName, IContentTypeSettings.FILE_NAME_SPEC));
}
Set<ContentType> selectedByName = selectMatchingByName(context, allByFileName, Collections.emptySet(), fileName,
IContentType.FILE_NAME_SPEC);
result[0] = selectedByName.toArray(new IContentType[selectedByName.size()]);
final String fileExtension = ContentTypeManager.getFileExtension(fileName);
if (fileExtension != null) {
final Set<ContentType> allByFileExtension;
if (context.equals(manager.getContext()))
allByFileExtension = getDirectlyAssociated(fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC);
else {
allByFileExtension = new HashSet<>(getDirectlyAssociated(fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC | IContentType.IGNORE_USER_DEFINED));
allByFileExtension.addAll(matcher.getDirectlyAssociated(this, fileExtension, IContentTypeSettings.FILE_EXTENSION_SPEC));
}
Set<ContentType> selectedByExtension = selectMatchingByName(context, allByFileExtension, selectedByName, fileExtension, IContentType.FILE_EXTENSION_SPEC);
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);
return result;
}
/**
* Returns content types directly associated with the given file spec.
*
* @param text a file name or extension
* @param typeMask a bit-wise or of the following flags:
* <ul>
* <li>IContentType.FILE_NAME, </li>
* <li>IContentType.FILE_EXTENSION, </li>
* <li>IContentType.IGNORE_PRE_DEFINED, </li>
* <li>IContentType.IGNORE_USER_DEFINED</li>
* </ul>
* @return a set of content types
*/
private Set<ContentType> getDirectlyAssociated(String text, int typeMask) {
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();
}
}
}
return result == null ? Collections.EMPTY_SET : result;
}
synchronized ContentType internalGetContentType(String contentTypeIdentifier) {
return (ContentType) contentTypes.get(contentTypeIdentifier);
}
private void makeAliases() {
// process all content types marking aliases appropriately
for (Iterator<IContentType> i = contentTypes.values().iterator(); i.hasNext();) {
ContentType type = (ContentType) i.next();
String targetId = type.getAliasTargetId();
if (targetId == null)
continue;
ContentType target = internalGetContentType(targetId);
if (target != null)
type.setAliasTarget(target);
}
}
/**
* Resolves inter-content type associations (inheritance and aliasing).
*/
synchronized protected void organize() {
// build the aliasing
makeAliases();
// do the validation
for (Iterator<IContentType> i = contentTypes.values().iterator(); i.hasNext();) {
ContentType type = (ContentType) i.next();
if (ensureValid(type))
associate(type);
}
if (ContentTypeManager.DEBUGGING)
for (Iterator<IContentType> i = contentTypes.values().iterator(); i.hasNext();) {
ContentType type = (ContentType) i.next();
if (!type.isValid())
ContentMessages.message("Invalid: " + type); //$NON-NLS-1$
}
}
/**
* Processes all content types in source, adding those matching the given file spec to the
* destination collection.
*/
private Set<ContentType> selectMatchingByName(final IScopeContext context, Collection<ContentType> source, final Collection<ContentType> existing, final String fileSpecText, final int fileSpecType) {
if (source == null || source.isEmpty())
return Collections.EMPTY_SET;
final Set<ContentType> destination = new HashSet<>(5);
// process all content types in the given collection
for (Iterator<ContentType> i = source.iterator(); i.hasNext();) {
final ContentType root = i.next();
// From a given content type, check if it matches, and
// include any children that match as well.
internalAccept(new ContentTypeVisitor() {
@Override
public int visit(ContentType type) {
if (type != root && type.hasBuiltInAssociations())
// this content type has built-in associations - visit it later as root
return RETURN;
if (type == root && !type.hasFileSpec(context, fileSpecText, fileSpecType))
// it is the root and does not match the file name - do not add it nor look into its children
return RETURN;
// either the content type is the root and matches the file name or
// is a sub content type and does not have built-in files specs
if (!existing.contains(type))
destination.add(type);
return CONTINUE;
}
}, root);
}
return destination;
}
void removeContentType(String contentTypeIdentifier) throws CoreException {
ContentType contentType = getContentType(contentTypeIdentifier);
if (contentType == null) {
return;
}
if (!contentType.isUserDefined()) {
throw new IllegalArgumentException("Content type must be user-defined."); //$NON-NLS-1$
}
contentTypes.remove(contentType.getId());
}
}