blob: 66c615c97ebf6467592a6c0363eabc3a64265e72 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2009 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.internal.runtime.RuntimeLog;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.content.*;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.osgi.util.NLS;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
public final class ContentType implements IContentType, IContentTypeInfo {
/* A placeholder for missing/invalid binary/text describers. */
private class InvalidDescriber implements IContentDescriber, ITextContentDescriber {
@Override
public int describe(InputStream contents, IContentDescription description) {
return INVALID;
}
@Override
public int describe(Reader contents, IContentDescription description) {
return INVALID;
}
@Override
public QualifiedName[] getSupportedOptions() {
return new QualifiedName[0];
}
}
final static byte ASSOCIATED_BY_EXTENSION = 2;
final static byte ASSOCIATED_BY_NAME = 1;
private static final String DESCRIBER_ELEMENT = "describer"; //$NON-NLS-1$
private static ArrayList<FileSpec> EMPTY_LIST = new ArrayList<>(0);
private static final Object INHERITED_DESCRIBER = "INHERITED DESCRIBER"; //$NON-NLS-1$
private static final Object NO_DESCRIBER = "NO DESCRIBER"; //$NON-NLS-1$
final static byte NOT_ASSOCIATED = 0;
public final static String PREF_DEFAULT_CHARSET = "charset"; //$NON-NLS-1$
public final static String PREF_FILE_EXTENSIONS = "file-extensions"; //$NON-NLS-1$
public final static String PREF_FILE_NAMES = "file-names"; //$NON-NLS-1$
/** @since 3.7 */
public final static String PREF_FILE_PATTERNS = "file-patterns"; //$NON-NLS-1$
/** @since 3.6 */
public static final String PREF_USER_DEFINED = "userDefined"; //$NON-NLS-1$
/** @since 3.6 */
public static final String PREF_USER_DEFINED__SEPARATOR = ","; //$NON-NLS-1$
/** @since 3.6 */
public static final String PREF_USER_DEFINED__NAME = "name"; //$NON-NLS-1$
/** @since 3.6 */
public static final String PREF_USER_DEFINED__BASE_TYPE_ID = "baseTypeId"; //$NON-NLS-1$
final static byte PRIORITY_HIGH = 1;
final static byte PRIORITY_LOW = -1;
final static byte PRIORITY_NORMAL = 0;
final static int SPEC_PRE_DEFINED = IGNORE_PRE_DEFINED;
final static int SPEC_USER_DEFINED = IGNORE_USER_DEFINED;
final static byte STATUS_INVALID = 2;
final static byte STATUS_UNKNOWN = 0;
final static byte STATUS_VALID = 1;
static final String EMPTY_STRING = ""; //$NON-NLS-1$
private String aliasTargetId;
private String baseTypeId;
private boolean builtInAssociations = false;
private ContentTypeCatalog catalog;
private IConfigurationElement contentTypeElement;
private DefaultDescription defaultDescription;
private Map<QualifiedName, String> defaultProperties;
private Object describer;
// we need a Cloneable list
private ArrayList<FileSpec> fileSpecs = EMPTY_LIST;
String id;
private ContentTypeManager manager;
private String name;
private byte priority;
private ContentType target;
private String userCharset;
private byte validation = STATUS_UNKNOWN;
private ContentType baseType;
// -1 means unknown
private byte depth = -1;
public static ContentType createContentType(ContentTypeCatalog catalog, String uniqueId, String name, byte priority,
String[] fileExtensions, String[] fileNames, String[] filePatterns, String baseTypeId, String aliasTargetId,
Map<QualifiedName, String> defaultProperties, IConfigurationElement contentTypeElement) {
ContentType contentType = new ContentType(catalog.getManager());
contentType.catalog = catalog;
contentType.defaultDescription = new DefaultDescription(contentType);
contentType.id = uniqueId;
contentType.name = name;
contentType.priority = priority;
if ((fileExtensions != null && fileExtensions.length > 0) || (fileNames != null && fileNames.length > 0)
|| (filePatterns != null && filePatterns.length > 0)) {
contentType.builtInAssociations = true;
contentType.fileSpecs = new ArrayList<>(fileExtensions.length + fileNames.length + filePatterns.length);
for (String fileName : fileNames)
contentType.internalAddFileSpec(fileName, FILE_NAME_SPEC | SPEC_PRE_DEFINED);
for (String fileExtension : fileExtensions)
contentType.internalAddFileSpec(fileExtension, FILE_EXTENSION_SPEC | SPEC_PRE_DEFINED);
for (String fileExtension : filePatterns) {
contentType.internalAddFileSpec(fileExtension, FILE_PATTERN_SPEC | SPEC_PRE_DEFINED);
}
}
contentType.defaultProperties = defaultProperties;
contentType.contentTypeElement = contentTypeElement;
contentType.baseTypeId = baseTypeId;
contentType.aliasTargetId = aliasTargetId;
return contentType;
}
static FileSpec createFileSpec(String fileSpec, int type) {
return new FileSpec(fileSpec, type);
}
static String getPreferenceKey(int flags) {
if ((flags & FILE_EXTENSION_SPEC) != 0)
return PREF_FILE_EXTENSIONS;
if ((flags & FILE_NAME_SPEC) != 0)
return PREF_FILE_NAMES;
if ((flags & FILE_PATTERN_SPEC) != 0)
return PREF_FILE_PATTERNS;
throw new IllegalArgumentException("Unknown type: " + flags); //$NON-NLS-1$
}
private static String getValidationString(byte validation) {
return validation == STATUS_VALID ? "VALID" : (validation == STATUS_INVALID ? "INVALID" : "UNKNOWN"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
public static void log(String message, Throwable reason) {
// don't log CoreExceptions again
IStatus status = new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, reason instanceof CoreException ? null : reason);
RuntimeLog.log(status);
}
public ContentType(ContentTypeManager manager) {
this.manager = manager;
}
@Override
public void addFileSpec(String fileSpec, int type) throws CoreException {
Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_SPEC || type == FILE_PATTERN_SPEC,
"Unknown type: " + type); //$NON-NLS-1$
String[] userSet;
synchronized (this) {
if (!internalAddFileSpec(fileSpec, type | SPEC_USER_DEFINED))
return;
userSet = getFileSpecs(type | IGNORE_PRE_DEFINED);
}
// persist using preferences
Preferences contentTypeNode = manager.getPreferences().node(id);
String newValue = Util.toListString(userSet);
// we are adding stuff, newValue must be non-null
Assert.isNotNull(newValue);
setPreference(contentTypeNode, getPreferenceKey(type), newValue);
try {
contentTypeNode.flush();
} catch (BackingStoreException bse) {
String message = NLS.bind(ContentMessages.content_errorSavingSettings, id);
IStatus status = new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, bse);
throw new CoreException(status);
}
// notify listeners
manager.fireContentTypeChangeEvent(this);
}
int describe(IContentDescriber selectedDescriber, ILazySource contents, ContentDescription description) throws IOException {
try {
return contents.isText() ? ((ITextContentDescriber) selectedDescriber).describe((Reader) contents, description) : selectedDescriber.describe((InputStream) contents, description);
} catch (RuntimeException re) {
// describer seems to be buggy. just disable it (logging the reason)
invalidateDescriber(re);
} catch (Error e) {
// describer got some serious problem. disable it (logging the reason) and throw the error again
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, id);
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;
}
@Override
public boolean equals(Object another) {
if (another instanceof ContentType)
return id.equals(((ContentType) another).id);
if (another instanceof ContentTypeHandler)
return id.equals(((ContentTypeHandler) another).id);
return false;
}
public String getAliasTargetId() {
return aliasTargetId;
}
@Override
public IContentType getBaseType() {
return baseType;
}
String getBaseTypeId() {
return baseTypeId;
}
public ContentTypeCatalog getCatalog() {
return catalog;
}
@Override
public ContentType getContentType() {
return this;
}
@Override
public String getDefaultCharset() {
return getDefaultProperty(IContentDescription.CHARSET);
}
@Override
public IContentDescription getDefaultDescription() {
return defaultDescription;
}
/**
* Returns the default value for the given property in this content type, or <code>null</code>.
*/
@Override
public String getDefaultProperty(QualifiedName key) {
String propertyValue = internalGetDefaultProperty(key);
if ("".equals(propertyValue)) //$NON-NLS-1$
return null;
return propertyValue;
}
byte getDepth() {
byte tmpDepth = depth;
if (tmpDepth >= 0)
return tmpDepth;
// depth was never computed - do it now
if (baseType == null)
return depth = 0;
return depth = (byte) (baseType == null ? 0 : (1 + baseType.getDepth()));
}
/**
* Public for tests only, should not be called by anyone else.
*/
public IContentDescriber getDescriber() {
try {
// thread safety
Object tmpDescriber = describer;
if (tmpDescriber != null) {
if (INHERITED_DESCRIBER == tmpDescriber)
return baseType.getDescriber();
return (NO_DESCRIBER == tmpDescriber) ? null : (IContentDescriber) tmpDescriber;
}
final String describerValue = contentTypeElement != null
? contentTypeElement.getAttribute(DESCRIBER_ELEMENT)
: null;
IConfigurationElement[] childrenDescribers = contentTypeElement != null
? contentTypeElement.getChildren(DESCRIBER_ELEMENT)
: new IConfigurationElement[0];
if (describerValue != null || childrenDescribers.length > 0)
try {
if ("".equals(describerValue)) { //$NON-NLS-1$
describer = NO_DESCRIBER;
return null;
}
describer = tmpDescriber = contentTypeElement.createExecutableExtension(DESCRIBER_ELEMENT);
return (IContentDescriber) tmpDescriber;
} catch (CoreException ce) {
// the content type definition was invalid. Ensure we don't
// try again, and this content type does not accept any
// contents
return invalidateDescriber(ce);
}
} catch (InvalidRegistryObjectException e) {
/*
* This should only happen if an API call is made after the registry has changed and before
* the corresponding registry change event has been broadcast.
*/
// the configuration element is stale - need to rebuild the catalog
manager.invalidate();
// bad timing - next time the client asks for a describer, s/he will have better luck
return null;
}
if (baseType == null) {
describer = NO_DESCRIBER;
return null;
}
// remember so we don't have to come all the way down here next time
describer = INHERITED_DESCRIBER;
return baseType.getDescriber();
}
@Override
public IContentDescription getDescriptionFor(InputStream contents, QualifiedName[] options) throws IOException {
return internalGetDescriptionFor(ContentTypeManager.readBuffer(contents), options);
}
@Override
public IContentDescription getDescriptionFor(Reader contents, QualifiedName[] options) throws IOException {
return internalGetDescriptionFor(ContentTypeManager.readBuffer(contents), options);
}
@Override
public String[] getFileSpecs(int typeMask) {
if (fileSpecs.isEmpty())
return new String[0];
// invert the last two bits so it is easier to compare
typeMask ^= (IGNORE_PRE_DEFINED | IGNORE_USER_DEFINED);
List<String> result = new ArrayList<>(fileSpecs.size());
for (FileSpec spec : fileSpecs) {
if ((spec.getType() & typeMask) == spec.getType())
result.add(spec.getText());
}
return result.toArray(new String[result.size()]);
}
@Override
public String getId() {
return id;
}
@Override
public String getName() {
return name;
}
byte getPriority() {
return priority;
}
@Override
public IContentTypeSettings getSettings(IScopeContext context) {
if (context == null || context.equals(manager.getContext()))
return this;
return new ContentTypeSettings(this, context);
}
/*
* Returns the alias target, if one is found, or this object otherwise.
*/
ContentType getAliasTarget(boolean self) {
return (self && target == null) ? this : target;
}
byte getValidation() {
return validation;
}
boolean hasBuiltInAssociations() {
return builtInAssociations;
}
boolean hasFileSpec(IScopeContext context, String text, int typeMask) {
if (context.equals(manager.getContext()) || (typeMask & IGNORE_USER_DEFINED) != 0)
return hasFileSpec(text, typeMask, false);
String[] fileSpecs = ContentTypeSettings.getFileSpecs(context, id, typeMask);
for (String fileSpec : fileSpecs)
if (text.equalsIgnoreCase(fileSpec))
return true;
// no user defined association... try built-in
return hasFileSpec(text, typeMask | IGNORE_PRE_DEFINED, false);
}
/**
* Returns whether this content type has the given file spec.
*
* @param text
* the file spec string
* @param typeMask
* FILE_NAME_SPEC or FILE_EXTENSION_SPEC or FILE_REGEXP_SPEC
* @param strict
* @return true if this file spec has already been added, false otherwise
*/
boolean hasFileSpec(String text, int typeMask, boolean strict) {
if (fileSpecs.isEmpty())
return false;
for (FileSpec spec : fileSpecs) {
if (spec.equals(text, typeMask, strict))
return true;
}
return false;
}
@Override
public int hashCode() {
return id.hashCode();
}
/**
* Adds a user-defined or pre-defined file spec.
*/
boolean internalAddFileSpec(String fileSpec, int typeMask) {
if (hasFileSpec(fileSpec, typeMask, false))
return false;
FileSpec newFileSpec = createFileSpec(fileSpec, typeMask);
if ((typeMask & ContentType.SPEC_USER_DEFINED) == 0) {
// plug-in defined - all that is left to be done is to add it to the list
if (fileSpecs.isEmpty())
fileSpecs = new ArrayList<>(3);
fileSpecs.add(newFileSpec);
return true;
}
// update file specs atomically so threads traversing the list of file specs don't have to synchronize
@SuppressWarnings("unchecked")
ArrayList<FileSpec> tmpFileSpecs = (ArrayList<FileSpec>) fileSpecs.clone();
tmpFileSpecs.add(newFileSpec);
catalog.associate(this, newFileSpec.getText(), newFileSpec.getType());
// set the new file specs atomically
fileSpecs = tmpFileSpecs;
return true;
}
/**
* Returns the default value for a property, recursively if necessary.
*/
String internalGetDefaultProperty(QualifiedName key) {
// a special case for charset - users can override
if (userCharset != null && key.equals(IContentDescription.CHARSET))
return userCharset;
String defaultValue = basicGetDefaultProperty(key);
if (defaultValue != null)
return defaultValue;
// not defined here, try base type
return baseType == null ? null : baseType.internalGetDefaultProperty(key);
}
/**
* Returns the value of a built-in property defined for this content type.
*/
String basicGetDefaultProperty(QualifiedName key) {
return defaultProperties == null ? null : defaultProperties.get(key);
}
BasicDescription internalGetDescriptionFor(ILazySource buffer, QualifiedName[] options) throws IOException {
if (buffer == null)
return defaultDescription;
// use temporary local var to avoid sync'ing
IContentDescriber tmpDescriber = this.getDescriber();
// no describer - return default description
if (tmpDescriber == null)
return defaultDescription;
if (buffer.isText() && !(tmpDescriber instanceof ITextContentDescriber))
// it is an error to provide a Reader to a non-text content type
throw new UnsupportedOperationException();
ContentDescription description = new ContentDescription(options, this);
if (describe(tmpDescriber, buffer, description) == IContentDescriber.INVALID)
// the contents were actually invalid for the content type
return null;
// the describer didn't add any details, return default description
if (!description.isSet())
return defaultDescription;
// description cannot be changed afterwards
description.markImmutable();
return description;
}
byte internalIsAssociatedWith(String fileName, IScopeContext context) {
if (hasFileSpec(context, fileName, FILE_NAME_SPEC))
return ASSOCIATED_BY_NAME;
String fileExtension = ContentTypeManager.getFileExtension(fileName);
if (hasFileSpec(context, fileExtension, FILE_EXTENSION_SPEC))
return ASSOCIATED_BY_EXTENSION;
// if does not have built-in file specs, delegate to parent (if any)
if (!hasBuiltInAssociations() && baseType != null)
return baseType.internalIsAssociatedWith(fileName, context);
return NOT_ASSOCIATED;
}
boolean internalRemoveFileSpec(String fileSpec, int typeMask) {
if (fileSpecs.isEmpty())
return false;
// we modify the list of file specs atomically so we don't interfere with threads doing traversals
@SuppressWarnings("unchecked")
ArrayList<FileSpec> tmpFileSpecs = (ArrayList<FileSpec>) fileSpecs.clone();
for (Iterator<FileSpec> i = tmpFileSpecs.iterator(); i.hasNext();) {
FileSpec spec = i.next();
if ((spec.getType() == typeMask) && fileSpec.equals(spec.getText())) {
i.remove();
catalog.dissociate(this, spec.getText(), spec.getType());
// update the list of file specs
fileSpecs = tmpFileSpecs;
return true;
}
}
return false;
}
public IContentDescriber invalidateDescriber(Throwable reason) {
String message = NLS.bind(ContentMessages.content_invalidContentDescriber, id);
log(message, reason);
return (IContentDescriber) (describer = new InvalidDescriber());
}
boolean isAlias() {
return target != null;
}
@Override
public boolean isAssociatedWith(String fileName) {
return isAssociatedWith(fileName, manager.getContext());
}
@Override
public boolean isAssociatedWith(String fileName, IScopeContext context) {
return internalIsAssociatedWith(fileName, context) != NOT_ASSOCIATED;
}
@Override
public boolean isKindOf(IContentType another) {
if (another == null)
return false;
if (this == another)
return true;
return baseType != null && baseType.isKindOf(another);
}
boolean isValid() {
return validation == STATUS_VALID;
}
void processPreferences(Preferences contentTypeNode) {
// user set default charset
this.userCharset = contentTypeNode.get(PREF_DEFAULT_CHARSET, null);
// user set file names
String userSetFileNames = contentTypeNode.get(PREF_FILE_NAMES, null);
String[] fileNames = Util.parseItems(userSetFileNames);
for (String fileName : fileNames)
internalAddFileSpec(fileName, FILE_NAME_SPEC | SPEC_USER_DEFINED);
// user set file extensions
String userSetFileExtensions = contentTypeNode.get(PREF_FILE_EXTENSIONS, null);
String[] fileExtensions = Util.parseItems(userSetFileExtensions);
for (String fileExtension : fileExtensions)
internalAddFileSpec(fileExtension, FILE_EXTENSION_SPEC | SPEC_USER_DEFINED);
// user set file name regexp
String userSetFileRegexp = contentTypeNode.get(PREF_FILE_PATTERNS, null);
String[] fileRegexps = Util.parseItems(userSetFileRegexp);
for (String fileRegexp : fileRegexps) {
internalAddFileSpec(fileRegexp, FILE_PATTERN_SPEC | SPEC_USER_DEFINED);
}
}
@Override
public void removeFileSpec(String fileSpec, int type) throws CoreException {
Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_SPEC || type == FILE_PATTERN_SPEC,
"Unknown type: " + type); //$NON-NLS-1$
synchronized (this) {
if (!internalRemoveFileSpec(fileSpec, type | SPEC_USER_DEFINED))
return;
}
// persist the change
Preferences contentTypeNode = manager.getPreferences().node(id);
final String[] userSet = getFileSpecs(type | IGNORE_PRE_DEFINED);
String preferenceKey = getPreferenceKey(type);
String newValue = Util.toListString(userSet);
setPreference(contentTypeNode, preferenceKey, newValue);
try {
contentTypeNode.flush();
} catch (BackingStoreException bse) {
String message = NLS.bind(ContentMessages.content_errorSavingSettings, id);
IStatus status = new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, bse);
throw new CoreException(status);
}
// notify listeners
manager.fireContentTypeChangeEvent(this);
}
void setAliasTarget(ContentType newTarget) {
target = newTarget;
}
@Override
public void setDefaultCharset(String newCharset) throws CoreException {
synchronized (this) {
// don't do anything if there is no actual change
if (userCharset == null) {
if (newCharset == null)
return;
} else if (userCharset.equals(newCharset))
return;
// apply change in memory
userCharset = newCharset;
}
// persist the change
Preferences contentTypeNode = manager.getPreferences().node(id);
setPreference(contentTypeNode, PREF_DEFAULT_CHARSET, userCharset);
try {
contentTypeNode.flush();
} catch (BackingStoreException bse) {
String message = NLS.bind(ContentMessages.content_errorSavingSettings, id);
IStatus status = new Status(IStatus.ERROR, ContentMessages.OWNER_NAME, 0, message, bse);
throw new CoreException(status);
}
// notify listeners
manager.fireContentTypeChangeEvent(this);
}
static void setPreference(Preferences node, String key, String value) {
if (value == null)
node.remove(key);
else
node.put(key, value);
}
void setValidation(byte validation) {
this.validation = validation;
if (ContentTypeManager.DEBUGGING)
ContentMessages.message("Validating " + this + ": " + getValidationString(validation)); //$NON-NLS-1$ //$NON-NLS-2$
}
@Override
public String toString() {
return id;
}
void setBaseType(ContentType baseType) {
this.baseType = baseType;
}
@Override
public boolean isUserDefined() {
return this.contentTypeElement == null;
}
}