| /******************************************************************************* |
| * Copyright (c) 2004, 2005 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.*; |
| 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; |
| |
| /** |
| * @see IContentType |
| */ |
| public final class ContentType implements IContentType, IContentTypeInfo { |
| |
| /* A placeholder for missing/invalid binary/text describers. */ |
| private class InvalidDescriber implements IContentDescriber, ITextContentDescriber { |
| public int describe(InputStream contents, IContentDescription description) { |
| return INVALID; |
| } |
| |
| public int describe(Reader contents, IContentDescription description) { |
| return INVALID; |
| } |
| |
| 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 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$ |
| 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; |
| private String aliasTargetId; |
| private String baseTypeId; |
| private boolean builtInAssociations = false; |
| private ContentTypeCatalog catalog; |
| private IConfigurationElement contentTypeElement; |
| private DefaultDescription defaultDescription; |
| private Map defaultProperties; |
| private Object describer; |
| private List fileSpecs; |
| 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 baseTypeId, String aliasTargetId, Map 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)) { |
| contentType.builtInAssociations = true; |
| contentType.fileSpecs = new ArrayList(fileExtensions.length + fileNames.length); |
| for (int i = 0; i < fileNames.length; i++) |
| contentType.internalAddFileSpec(fileNames[i], FILE_NAME_SPEC | SPEC_PRE_DEFINED); |
| for (int i = 0; i < fileExtensions.length; i++) |
| contentType.internalAddFileSpec(fileExtensions[i], FILE_EXTENSION_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; |
| 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, Platform.PI_RUNTIME, 0, message, reason instanceof CoreException ? null : reason); |
| InternalPlatform.getDefault().log(status); |
| } |
| |
| public ContentType(ContentTypeManager manager) { |
| this.manager = manager; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public void addFileSpec(String fileSpec, int type) throws CoreException { |
| Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_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); |
| contentTypeNode.put(getPreferenceKey(type), newValue); |
| try { |
| contentTypeNode.flush(); |
| } catch (BackingStoreException bse) { |
| String message = NLS.bind(Messages.content_errorSavingSettings, id); |
| IStatus status = new Status(IStatus.ERROR, Platform.PI_RUNTIME, 0, message, bse); |
| throw new CoreException(status); |
| } |
| // notify listeners |
| manager.fireContentTypeChangeEvent(this); |
| } |
| |
| int describe(IContentDescriber selectedDescriber, ILazySource contents, ContentDescription description) throws IOException { |
| final boolean isText = contents.isText(); |
| if (isText && !(selectedDescriber instanceof ITextContentDescriber)) |
| throw new UnsupportedOperationException(); |
| try { |
| return 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(Messages.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; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| |
| public IContentType getBaseType() { |
| return baseType; |
| } |
| |
| String getBaseTypeId() { |
| return baseTypeId; |
| } |
| |
| public ContentTypeCatalog getCatalog() { |
| return catalog; |
| } |
| |
| public ContentType getContentType() { |
| return this; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public String getDefaultCharset() { |
| return getDefaultProperty(IContentDescription.CHARSET); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public IContentDescription getDefaultDescription() { |
| return defaultDescription; |
| } |
| |
| /** |
| * Returns the default value for the given property in this content type, or <code>null</code>. |
| */ |
| 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.getAttributeAsIs(DESCRIBER_ELEMENT); |
| if (describerValue != null || contentTypeElement.getChildren(DESCRIBER_ELEMENT).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(); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public IContentDescription getDescriptionFor(InputStream contents, QualifiedName[] options) throws IOException { |
| return internalGetDescriptionFor(ContentTypeManager.readBuffer(contents), options); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public IContentDescription getDescriptionFor(Reader contents, QualifiedName[] options) throws IOException { |
| return internalGetDescriptionFor(ContentTypeManager.readBuffer(contents), options); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public String[] getFileSpecs(int typeMask) { |
| if (fileSpecs == null) |
| return new String[0]; |
| // invert the last two bits so it is easier to compare |
| typeMask ^= (IGNORE_PRE_DEFINED | IGNORE_USER_DEFINED); |
| List result = new ArrayList(fileSpecs.size()); |
| for (Iterator i = fileSpecs.iterator(); i.hasNext();) { |
| FileSpec spec = (FileSpec) i.next(); |
| if ((spec.getType() & typeMask) == spec.getType()) |
| result.add(spec.getText()); |
| } |
| return (String[]) result.toArray(new String[result.size()]); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public String getId() { |
| return id; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public String getName() { |
| return name; |
| } |
| |
| byte getPriority() { |
| return priority; |
| } |
| |
| 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 (int i = 0; i < fileSpecs.length; i++) |
| if (text.equalsIgnoreCase(fileSpecs[i])) |
| 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 |
| * @param strict |
| * @return true if this file spec has already been added, false otherwise |
| */ |
| boolean hasFileSpec(String text, int typeMask, boolean strict) { |
| if (fileSpecs == null) |
| return false; |
| for (Iterator i = fileSpecs.iterator(); i.hasNext();) { |
| FileSpec spec = (FileSpec) i.next(); |
| if (spec.equals(text, typeMask, strict)) |
| return true; |
| } |
| return false; |
| } |
| |
| 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; |
| if (fileSpecs == null) |
| fileSpecs = new ArrayList(3); |
| FileSpec newFileSpec = createFileSpec(fileSpec, typeMask); |
| fileSpecs.add(newFileSpec); |
| if ((typeMask & ContentType.SPEC_USER_DEFINED) != 0) |
| catalog.associate(this, newFileSpec.getText(), newFileSpec.getType()); |
| 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 : (String) 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; |
| 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) { |
| if (hasFileSpec(fileName, FILE_NAME_SPEC, false)) |
| return ASSOCIATED_BY_NAME; |
| String fileExtension = ContentTypeManager.getFileExtension(fileName); |
| if (hasFileSpec(fileExtension, FILE_EXTENSION_SPEC, false)) |
| 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); |
| return NOT_ASSOCIATED; |
| } |
| |
| boolean internalRemoveFileSpec(String fileSpec, int typeMask) { |
| if (fileSpecs == null) |
| return false; |
| for (Iterator i = fileSpecs.iterator(); i.hasNext();) { |
| FileSpec spec = (FileSpec) i.next(); |
| if ((spec.getType() == typeMask) && fileSpec.equals(spec.getText())) { |
| i.remove(); |
| catalog.dissociate(this, spec.getText(), spec.getType()); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private IContentDescriber invalidateDescriber(Throwable reason) { |
| String message = NLS.bind(Messages.content_invalidContentDescriber, id); |
| log(message, reason); |
| return (IContentDescriber) (describer = new InvalidDescriber()); |
| } |
| |
| boolean isAlias() { |
| return target != null; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public boolean isAssociatedWith(String fileName) { |
| return internalIsAssociatedWith(fileName) != NOT_ASSOCIATED; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public boolean isAssociatedWith(String fileName, IScopeContext context) { |
| //TODO should honor context parameter |
| return isAssociatedWith(fileName); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| 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 (int i = 0; i < fileNames.length; i++) |
| internalAddFileSpec(fileNames[i], FILE_NAME_SPEC | SPEC_USER_DEFINED); |
| // user set file extensions |
| String userSetFileExtensions = contentTypeNode.get(PREF_FILE_EXTENSIONS, null); |
| String[] fileExtensions = Util.parseItems(userSetFileExtensions); |
| for (int i = 0; i < fileExtensions.length; i++) |
| internalAddFileSpec(fileExtensions[i], FILE_EXTENSION_SPEC | SPEC_USER_DEFINED); |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| public void removeFileSpec(String fileSpec, int type) throws CoreException { |
| Assert.isLegal(type == FILE_EXTENSION_SPEC || type == FILE_NAME_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); |
| if (newValue == null) |
| contentTypeNode.remove(preferenceKey); |
| else |
| contentTypeNode.put(preferenceKey, newValue); |
| try { |
| contentTypeNode.flush(); |
| } catch (BackingStoreException bse) { |
| String message = NLS.bind(Messages.content_errorSavingSettings, id); |
| IStatus status = new Status(IStatus.ERROR, Platform.PI_RUNTIME, 0, message, bse); |
| throw new CoreException(status); |
| } |
| // notify listeners |
| manager.fireContentTypeChangeEvent(this); |
| } |
| |
| void setAliasTarget(ContentType newTarget) { |
| target = newTarget; |
| } |
| |
| /** |
| * @see IContentType |
| */ |
| 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(Messages.content_errorSavingSettings, id); |
| IStatus status = new Status(IStatus.ERROR, Platform.PI_RUNTIME, 0, message, bse); |
| throw new CoreException(status); |
| } |
| // notify listeners |
| manager.fireContentTypeChangeEvent(this); |
| } |
| |
| private 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) |
| Policy.debug("Validating " + this + ": " + getValidationString(validation)); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| public String toString() { |
| return id; |
| } |
| |
| void setBaseType(ContentType baseType) { |
| this.baseType = baseType; |
| } |
| |
| } |