Copyright (c) 2004, 2009 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation
/******************************************************************************* * Copyright (c) 2004, 2009 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * 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
/** @since 3.7 */
public final static String PREF_FILE_PATTERNS = "file-patterns"; //$NON-NLS-1$
Since:3.6
/** @since 3.6 */
public static final String PREF_USER_DEFINED = "userDefined"; //$NON-NLS-1$
Since:3.6
/** @since 3.6 */
public static final String PREF_USER_DEFINED__SEPARATOR = ","; //$NON-NLS-1$
Since:3.6
/** @since 3.6 */
public static final String PREF_USER_DEFINED__NAME = "name"; //$NON-NLS-1$
Since:3.6
/** @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 null.
/** * 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 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.
Params:
  • text – the file spec string
  • typeMask – FILE_NAME_SPEC or FILE_EXTENSION_SPEC or FILE_REGEXP_SPEC
  • strict –
Returns:true if this file spec has already been added, false otherwise
/** * 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.
/** * 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.
/** * 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.
/** * 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; } }