blob: c140b4fdfeb5723838cc017217922f6c86730ebc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-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.InternalPlatform;
import org.eclipse.core.internal.runtime.Policy;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.content.*;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
public final class ContentType implements IContentType {
/* A placeholder for missing/invalid binary/text describers. */
private class InvalidDescriber implements IContentDescriber, ITextContentDescriber {
public int describe(InputStream contents, IContentDescription description) throws IOException {
return INVALID;
}
public int describe(Reader contents, IContentDescription description) throws IOException {
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$
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 = 3;
final static byte STATUS_VALID = 1;
private ContentType aliasTarget;
private String baseTypeId;
private IContentType[] children;
private IConfigurationElement contentTypeElement;
private String defaultCharset;
private String userCharset;
private IContentDescription defaultDescription;
private IContentDescriber describer;
private List fileSpecs;
private ContentTypeManager manager;
private String name;
private String namespace;
private byte priority;
private String simpleId;
private byte validation;
public static ContentType createContentType(ContentTypeManager manager, String namespace, String simpleId, String name, byte priority, String[] fileExtensions, String[] fileNames, String baseTypeId, String defaultCharset, IConfigurationElement contentTypeElement) {
ContentType contentType = new ContentType(manager);
contentType.defaultDescription = new DefaultDescription(contentType);
contentType.simpleId = simpleId;
contentType.namespace = namespace;
contentType.name = name;
contentType.priority = priority;
if ((fileExtensions != null && fileExtensions.length > 0) || (fileNames != null && fileNames.length > 0)) {
contentType.fileSpecs = new ArrayList(fileExtensions.length + fileNames.length);
for (int i = 0; i < fileNames.length; i++)
contentType.fileSpecs.add(createFileSpec(fileNames[i], FILE_NAME_SPEC | SPEC_PRE_DEFINED));
for (int i = 0; i < fileExtensions.length; i++)
contentType.fileSpecs.add(createFileSpec(fileExtensions[i], FILE_EXTENSION_SPEC | SPEC_PRE_DEFINED));
}
contentType.defaultCharset = defaultCharset;
contentType.contentTypeElement = contentTypeElement;
contentType.baseTypeId = baseTypeId;
contentType.processPreferences();
return contentType;
}
private void processPreferences() {
Preferences contentTypeNode = manager.getPreferences().node(getId());
// 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 = 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 = parseItems(userSetFileExtensions);
for (int i = 0; i < fileExtensions.length; i++)
internalAddFileSpec(fileExtensions[i], FILE_EXTENSION_SPEC | SPEC_USER_DEFINED);
}
static FileSpec createFileSpec(String fileSpec, int type) {
return new FileSpec(fileSpec, type);
}
private 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$
}
static String[] parseItems(String string) {
if (string == null)
return new String[0];
StringTokenizer tokenizer = new StringTokenizer(string, ","); //$NON-NLS-1$
if (!tokenizer.hasMoreTokens())
return new String[0];
String first = tokenizer.nextToken();
if (!tokenizer.hasMoreTokens())
return new String[] {first};
ArrayList items = new ArrayList();
items.add(first);
do {
items.add(tokenizer.nextToken());
} while (tokenizer.hasMoreTokens());
return (String[]) items.toArray(new String[items.size()]);
}
static String toListString(List list) {
if (list.isEmpty())
return ""; //$NON-NLS-1$
StringBuffer result = new StringBuffer();
for (Iterator i = list.iterator(); i.hasNext();) {
result.append(i.next());
result.append(',');
}
// ignore last comma
return result.substring(0, result.length() - 1);
}
static String toListString(Object[] list) {
if (list.length == 0)
return ""; //$NON-NLS-1$
StringBuffer result = new StringBuffer();
for (int i = 0; i < list.length; i++) {
result.append(list[i]);
result.append(',');
}
// ignore last comma
return result.substring(0, result.length() - 1);
}
public ContentType(ContentTypeManager manager) {
this.manager = manager;
}
public synchronized void addFileSpec(String fileSpec, int type) throws CoreException {
if (aliasTarget != null) {
getTarget().addFileSpec(fileSpec, type);
return;
}
if (type != FILE_EXTENSION_SPEC && type != FILE_NAME_SPEC)
throw new IllegalArgumentException("Unknown type: " + type); //$NON-NLS-1$
if (!internalAddFileSpec(fileSpec, type | SPEC_USER_DEFINED))
// the entry was already there, nothing to do...
return;
// notify listeners
manager.fireContentTypeChangeEvent(this);
// update preferences
String key = getPreferenceKey(type);
Preferences contentTypeNode = manager.getPreferences().node(getId());
final String[] userSet = internalGetFileSpecs(type | IGNORE_PRE_DEFINED);
contentTypeNode.put(key, toListString(userSet));
// persist preferences
try {
contentTypeNode.flush();
} catch (BackingStoreException bse) {
String message = Policy.bind("content.errorSavingSettings", getId()); //$NON-NLS-1$
IStatus status = new Status(IStatus.ERROR, Platform.PI_RUNTIME, 0, message, bse);
throw new CoreException(status);
}
}
int describe(IContentDescriber selectedDescriber, InputStream contents, ContentDescription description) {
try {
return selectedDescriber.describe(contents, description);
} catch (IOException ioe) {
if (ContentTypeManager.DEBUGGING) {
String message = Policy.bind("content.errorReadingContents", getId()); //$NON-NLS-1$
log(message, ioe);
}
return IContentDescriber.INVALID;
} catch (RuntimeException re) {
// describer seems to be buggy. just disable it (logging the reason)
invalidateDescriber(re);
return IContentDescriber.INVALID;
} catch (Error e) {
// describer got some serious problem. disable it (logging the reason) and throw the error again
invalidateDescriber(e);
throw e;
} finally {
((LazyInputStream) contents).rewind();
}
}
int describe(ITextContentDescriber selectedDescriber, Reader contents, ContentDescription description) {
try {
return selectedDescriber.describe(contents, description);
} catch (IOException ioe) {
if (ContentTypeManager.DEBUGGING) {
String message = Policy.bind("content.errorReadingContents", getId()); //$NON-NLS-1$
log(message, ioe);
}
return IContentDescriber.INVALID;
} catch (RuntimeException re) {
// describer seems to be buggy. just disable it (logging the reason)
invalidateDescriber(re);
return IContentDescriber.INVALID;
} catch (Error e) {
// describer got some serious problem. disable it (logging the reason) and throw the error again
invalidateDescriber(e);
throw e;
} finally {
((LazyReader) contents).rewind();
}
}
public IContentType getBaseType() {
if (aliasTarget != null)
return getTarget().getBaseType();
if (baseTypeId == null)
return null;
ContentType originalBaseType = manager.internalGetContentType(baseTypeId);
return originalBaseType != null ? originalBaseType.getTarget() : null;
}
String getBaseTypeId() {
return baseTypeId;
}
IContentType[] getChildren() {
if (children == null)
children = manager.getChildren(this);
return children;
}
/**
* @see IContentType
*/
public String getDefaultCharset() {
if (aliasTarget != null)
return getTarget().getDefaultCharset();
String currentCharset = userCharset != null ? userCharset : internalGetDefaultCharset();
// an empty string as charset means: no default charset
return "".equals(currentCharset) ? null : currentCharset; //$NON-NLS-1$
}
public IContentDescription getDefaultDescription() {
if (aliasTarget != null)
return getTarget().getDefaultDescription();
return defaultDescription;
}
public int getDepth() {
ContentType baseType = (ContentType) getBaseType();
if (baseType == null)
return 0;
return 1 + baseType.getDepth();
}
public IContentDescriber getDescriber() {
if (aliasTarget != null)
return getTarget().getDescriber();
// if "" is specified no describer should be created
if ("".equals(contentTypeElement.getAttributeAsIs(DESCRIBER_ELEMENT))) //$NON-NLS-1$
return null;
synchronized (this) {
if (describer != null)
return describer;
if (contentTypeElement.getChildren(DESCRIBER_ELEMENT).length > 0 || contentTypeElement.getAttributeAsIs(DESCRIBER_ELEMENT) != null)
try {
return describer = (IContentDescriber) contentTypeElement.createExecutableExtension(DESCRIBER_ELEMENT);
} 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);
}
}
ContentType baseType = (ContentType) getBaseType();
return baseType == null ? null : baseType.getDescriber();
}
/**
* @see IContentType
*/
public IContentDescription getDescriptionFor(InputStream contents, QualifiedName[] options) throws IOException {
InputStream buffer = ContentTypeManager.readBuffer(contents);
if (buffer == null)
return defaultDescription;
return internalGetDescriptionFor(buffer, options);
}
/**
* @see IContentType
*/
public IContentDescription getDescriptionFor(Reader contents, QualifiedName[] options) throws IOException {
Reader buffer = ContentTypeManager.readBuffer(contents);
if (buffer == null)
return defaultDescription;
return internalGetDescriptionFor(buffer, options);
}
public String[] getFileSpecs(int typeMask) {
if (aliasTarget != null)
return getTarget().getFileSpecs(typeMask);
return internalGetFileSpecs(typeMask);
}
private String[] internalGetFileSpecs(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()]);
}
public String getId() {
return namespace + '.' + simpleId;
}
public String getName() {
return name;
}
byte getPriority() {
return priority;
}
String getSimpleId() {
return simpleId;
}
/*
* Returns the alias target, if one is found, or this object otherwise.
*/
ContentType getTarget() {
if (aliasTarget == null)
return this;
return aliasTarget.getTarget();
}
byte getValidation() {
return validation;
}
/**
* @param text the file spec string
* @param typeMask FILE_NAME_SPEC or FILE_EXTENSION_SPEC
* @return true if this file spec has already been added, false otherwise
*/
private boolean hasFileSpec(String text, int typeMask) {
if (fileSpecs == null)
return false;
for (Iterator i = fileSpecs.iterator(); i.hasNext();) {
FileSpec spec = (FileSpec) i.next();
if (spec.equals(text, typeMask))
return true;
}
return false;
}
boolean internalAddFileSpec(String fileSpec, int typeMask) {
if (hasFileSpec(fileSpec, typeMask))
return false;
if (aliasTarget != null)
return aliasTarget.internalAddFileSpec(fileSpec, typeMask);
if (fileSpecs == null)
fileSpecs = new ArrayList(3);
fileSpecs.add(createFileSpec(fileSpec, typeMask));
return true;
}
private String internalGetDefaultCharset() {
if (defaultCharset == null) {
ContentType baseType = (ContentType) getBaseType();
return baseType == null ? null : baseType.getDefaultCharset();
}
return defaultCharset;
}
IContentDescription internalGetDescriptionFor(InputStream buffer, QualifiedName[] options) {
if (aliasTarget != null)
return getTarget().internalGetDescriptionFor(buffer, options);
if (buffer == null)
return defaultDescription;
IContentDescriber describer = this.getDescriber();
// no describer - just return the default description
if (describer == null)
return defaultDescription;
ContentDescription description = new ContentDescription(options);
describe(describer, buffer, description);
// if the describer didn't add any details, just return the default
// description
if (!description.isSet())
return defaultDescription;
// check if any of the defaults need to be applied
if (description.isRequested(IContentDescription.CHARSET) && description.getProperty(IContentDescription.CHARSET) == null)
description.setProperty(IContentDescription.CHARSET, getDefaultCharset());
description.setContentType(this);
return description;
}
IContentDescription internalGetDescriptionFor(Reader buffer, QualifiedName[] options) {
if (aliasTarget != null)
return getTarget().internalGetDescriptionFor(buffer, options);
if (buffer == null)
return defaultDescription;
IContentDescriber describer = this.getDescriber();
// no describer - just return the default description
if (describer == null)
return defaultDescription;
ContentDescription description = new ContentDescription(options);
if (!(describer instanceof ITextContentDescriber))
throw new UnsupportedOperationException();
describe((ITextContentDescriber) describer, buffer, description);
// if the describer didn't add any details, just return the default description
if (!description.isSet())
return defaultDescription;
// check if any of the defaults need to be applied
if (description.isRequested(IContentDescription.CHARSET) && description.getProperty(IContentDescription.CHARSET) == null)
description.setProperty(IContentDescription.CHARSET, getDefaultCharset());
description.setContentType(this);
return description;
}
public byte internalIsAssociatedWith(String fileName) {
if (aliasTarget != null)
return getTarget().internalIsAssociatedWith(fileName);
if (hasFileSpec(fileName, FILE_NAME_SPEC))
return ASSOCIATED_BY_NAME;
String fileExtension = ContentTypeManager.getFileExtension(fileName);
if (hasFileSpec(fileExtension, FILE_EXTENSION_SPEC))
return ASSOCIATED_BY_EXTENSION;
// if does not have *built-in* file specs, delegate to parent (if any)
if (!hasAnyFileSpec(SPEC_PRE_DEFINED)) {
IContentType baseType = getBaseType();
if (baseType != null)
return ((ContentType) baseType).internalIsAssociatedWith(fileName);
}
return NOT_ASSOCIATED;
}
private boolean hasAnyFileSpec(int typeMask) {
if (fileSpecs == null || fileSpecs.isEmpty())
return false;
for (Iterator i = fileSpecs.iterator(); i.hasNext();) {
FileSpec spec = (FileSpec) i.next();
if ((spec.getType() & typeMask) == typeMask)
return true;
}
return false;
}
boolean internalRemoveFileSpec(String fileSpec, int typeMask) {
if (aliasTarget != null)
return aliasTarget.internalRemoveFileSpec(fileSpec, 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();
return true;
}
}
return false;
}
private IContentDescriber invalidateDescriber(Throwable reason) {
setValidation(STATUS_INVALID);
String message = Policy.bind("content.invalidContentDescriber", getId()); //$NON-NLS-1$
log(message, reason);
return describer = new InvalidDescriber();
}
public boolean isAssociatedWith(String fileName) {
return internalIsAssociatedWith(fileName) != NOT_ASSOCIATED;
}
public boolean isKindOf(IContentType another) {
if (another == null)
return false;
if (aliasTarget != null)
return getTarget().isKindOf(another);
if (this == another)
return true;
if (getDepth() <= ((ContentType) another).getDepth())
return false;
IContentType baseType = getBaseType();
return baseType != null && baseType.isKindOf(another);
}
public boolean isText() {
return isKindOf(manager.getContentType(IContentTypeManager.CT_TEXT));
}
boolean isValid() {
return validation == STATUS_VALID;
}
private 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 synchronized void removeFileSpec(String fileSpec, int type) throws CoreException {
if (aliasTarget != null) {
getTarget().removeFileSpec(fileSpec, type);
return;
}
if (type != FILE_EXTENSION_SPEC && type != FILE_NAME_SPEC)
throw new IllegalArgumentException("Unknown type: " + type); //$NON-NLS-1$
if (!internalRemoveFileSpec(fileSpec, type | SPEC_USER_DEFINED))
// the entry was not there... nothing to do
return;
// notify listeners
manager.fireContentTypeChangeEvent(this);
// update preferences
String key = getPreferenceKey(type);
Preferences contentTypeNode = manager.getPreferences().node(getId());
final String[] userSet = internalGetFileSpecs(type | IGNORE_PRE_DEFINED);
if (userSet.length == 0)
contentTypeNode.remove(key);
else
contentTypeNode.put(key, toListString(userSet));
// persist using preferences
try {
contentTypeNode.flush();
} catch (BackingStoreException bse) {
String message = Policy.bind("content.errorSavingSettings", getId()); //$NON-NLS-1$
IStatus status = new Status(IStatus.ERROR, Platform.PI_RUNTIME, 0, message, bse);
throw new CoreException(status);
}
}
void setAliasTarget(ContentType newTarget) {
// when changing the target, it must be cleared first
if (aliasTarget != null && newTarget != null)
return;
// don't allow a sub-type to be made into an alias to the base type
if (aliasTarget == null && isKindOf(newTarget))
return;
if (ContentTypeManager.DEBUGGING)
Policy.debug("Setting alias target for " + this + " -> " + newTarget); //$NON-NLS-1$ //$NON-NLS-2$
aliasTarget = newTarget;
}
/*
* (non-Javadoc)
* @see org.eclipse.core.runtime.content.IContentType#setDefaultCharset(java.lang.String)
*/
public void setDefaultCharset(String newCharset) throws CoreException {
if (userCharset == null) {
if (newCharset == null)
return;
} else if (userCharset.equals(newCharset))
return;
userCharset = newCharset;
// notify listeners
manager.fireContentTypeChangeEvent(this);
// update preferences
Preferences contentTypeNode = manager.getPreferences().node(getId());
if (userCharset == null)
contentTypeNode.remove(PREF_DEFAULT_CHARSET);
else
contentTypeNode.put(PREF_DEFAULT_CHARSET, userCharset);
// persist preferences
try {
contentTypeNode.flush();
} catch (BackingStoreException bse) {
String message = Policy.bind("content.errorSavingSettings", getId()); //$NON-NLS-1$
IStatus status = new Status(IStatus.ERROR, Platform.PI_RUNTIME, 0, message, bse);
throw new CoreException(status);
}
}
void setValidation(byte validation) {
this.validation = validation;
if (ContentTypeManager.DEBUGGING)
Policy.debug("Validating " + this + ": " + getValidationString(validation)); //$NON-NLS-1$ //$NON-NLS-2$
}
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 String toString() {
return getId();
}
public boolean isAlias() {
return aliasTarget != null;
}
}