blob: 7113290003df86e54f34e190848d2c3ac24c752e [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.runtime.IPath;
import org.eclipse.core.runtime.IPlatform;
import org.eclipse.core.runtime.content.*;
import org.osgi.service.prefs.Preferences;
public class ContentTypeManager implements IContentTypeManager {
class ContentTypeComparator implements Comparator {
public int compare(Object o1, Object o2) {
ContentType type1 = (ContentType) o1;
ContentType type2 = (ContentType) o2;
if (type1.getPriority() != type2.getPriority())
return type1.getPriority() - type2.getPriority();
// to ensure stability
return type1.getId().compareTo(type2.getId());
}
}
final static String CONTENT_TYPE_PREF_NODE = IPlatform.PI_RUNTIME + IPath.SEPARATOR + "content-types"; //$NON-NLS-1$
private static ContentTypeManager instance;
private static final int MARK_LIMIT = 0x400;
private ContentTypeBuilder builder;
private Map catalog = new HashMap();
private Comparator comparator = new ContentTypeComparator();
private Map fileExtensions = new HashMap();
private Map fileNames = new HashMap();
/**
* Constructs a new content type manager.
*/
protected ContentTypeManager() {
builder = createBuilder();
}
protected void addContentType(IContentType contentType) {
catalog.put(contentType.getId(), contentType);
}
private void addFileSpecContributor(IContentType contentType, int fileSpecType, Map fileSpecsMap) {
String[] fileSpecs = contentType.getFileSpecs(fileSpecType);
for (int i = 0; i < fileSpecs.length; i++) {
Set existing = (Set) fileSpecsMap.get(fileSpecs[i]);
if (existing == null)
fileSpecsMap.put(fileSpecs[i], existing = new TreeSet(comparator));
existing.add(contentType);
}
}
protected ContentTypeBuilder createBuilder() {
return new ContentTypeBuilder(this);
}
private int describe(final IContentDescriber selectedDescriber, ByteArrayInputStream contents, ContentDescription description, int optionsMask) throws IOException {
try {
return selectedDescriber.describe(contents, description, optionsMask);
} finally {
contents.reset();
}
}
/**
* 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>
*/
private boolean ensureValid(ContentType type) {
if (type.getValidation() != ContentType.UNKNOWN)
return type.isValid();
if (type.getBaseTypeId() == null) {
type.setValidation(ContentType.VALID);
return true;
}
ContentType baseType = (ContentType) catalog.get(type.getBaseTypeId());
if (baseType == null) {
type.setValidation(ContentType.INVALID);
return false;
}
// set this temporarily as invalid to prevent cycles
// all types in the cycle will be marked as invalid
type.setValidation(ContentType.INVALID);
ensureValid(baseType);
// base type is either be valid or invalid - type will have the same status
type.setValidation(baseType.getValidation());
return type.isValid();
}
/*
* "public" to make testing easier
*/
public IContentType findContentTypeFor(InputStream contents, IContentType[] subset) throws IOException {
ByteArrayInputStream buffer = readBuffer(contents);
if (buffer == null)
return null;
if (subset == null)
subset = getAllContentTypes();
List appropriate = new ArrayList();
for (int i = 0; i < subset.length; i++) {
buffer.reset();
IContentDescriber describer = ((ContentType) subset[i]).getDescriber();
if (describer != null)
if (describe(describer, buffer, null, 0) == IContentDescriber.INVALID)
continue;
appropriate.add(subset[i]);
}
return mostAppropriate(appropriate);
}
/**
* @see IContentTypeManager
*/
public IContentType findContentTypeFor(InputStream contents, String fileName) throws IOException {
IContentType[] all = findContentTypesFor(contents, fileName);
return all.length > 0 ? all[0] : null;
}
/**
* @see IContentTypeManager
*/
public IContentType findContentTypeFor(String fileName) {
// basic implementation just gets all content types
IContentType[] associated = findContentTypesFor(fileName);
return associated.length == 0 ? null : associated[1];
}
/*
* "public" to make testing easier
*/
public IContentType[] findContentTypesFor(InputStream contents, IContentType[] subset) throws IOException {
if (subset.length == 0)
return new IContentType[0];
ByteArrayInputStream buffer = readBuffer(contents);
if (buffer == null)
return new IContentType[0];
return internalFindContentTypesFor(buffer, subset);
}
/**
* @see IContentTypeManager
*/
public IContentType[] findContentTypesFor(InputStream contents, String fileName) throws IOException {
IContentType[] subset = fileName != null ? findContentTypesFor(fileName) : getAllContentTypes();
ByteArrayInputStream buffer = readBuffer(contents);
if (buffer == null)
return new IContentType[0];
return internalFindContentTypesFor(buffer, subset);
}
/**
* @see IContentTypeManager
*/
public IContentType[] findContentTypesFor(String fileName) {
//TODO: should include children
IContentType[] result = new IContentType[2];
int count = 0;
// files associated by name should appear before those associated by extension
SortedSet allByFileName = (SortedSet) fileNames.get(fileName);
if (allByFileName != null && !allByFileName.isEmpty())
result[count++] = ((ContentType) allByFileName.first()).getTarget();
String fileExtension = getFileExtension(fileName);
if (fileExtension != null) {
SortedSet allByFileExtension = (SortedSet) fileExtensions.get(fileExtension);
if (allByFileExtension != null && !allByFileExtension.isEmpty())
result[count++] = ((ContentType) allByFileExtension.first()).getTarget();
}
if (count == 0)
return new IContentType[0];
if (count == 1 || result[0] == result[1])
return new IContentType[] {result[0]};
return result;
}
/**
* @see IContentTypeManager
*/
public IContentType[] getAllContentTypes() {
List result = new ArrayList(catalog.size());
for (Iterator i = catalog.values().iterator(); i.hasNext();) {
ContentType type = (ContentType) i.next();
if (type.isValid())
result.add(type);
}
return (IContentType[]) result.toArray(new IContentType[result.size()]);
}
/**
* @see IContentTypeManager
*/
public IContentType getContentType(String contentTypeIdentifier) {
ContentType type = internalGetContentType(contentTypeIdentifier);
return (type != null && type.isValid()) ? type : null;
}
/**
* @see IContentTypeManager
*/
public IContentDescription getDescriptionFor(InputStream contents, String fileName, int optionsMask) throws IOException {
// naïve implementation for now
ByteArrayInputStream buffer = readBuffer(contents);
if (buffer == null)
return null;
IContentType[] subset = fileName != null ? findContentTypesFor(fileName) : getAllContentTypes();
IContentType[] selected = internalFindContentTypesFor(buffer, subset);
if (selected.length == 0)
return null;
ContentDescription description = new ContentDescription();
description.setContentType(selected[0]);
// TODO less-than-optimal: causes stream to be read again
IContentDescriber selectedDescriber = ((ContentType) selected[0]).getDescriber();
if (selectedDescriber != null && (selectedDescriber.getSupportedOptions() & optionsMask) == optionsMask)
describe(selectedDescriber, buffer, description, optionsMask);
// check if any of the defaults need to be applied
if ((optionsMask & IContentDescription.CHARSET) != 0 && description.getCharset() == null)
description.setCharset(selected[0].getDefaultCharset());
description.markAsImmutable();
return description;
}
Preferences getPreferences() {
return InternalPlatform.getDefault().getPreferencesService().getRootNode().node(CONTENT_TYPE_PREF_NODE);
}
private IContentType[] internalFindContentTypesFor(ByteArrayInputStream buffer, IContentType[] subset) throws IOException {
List appropriate = new ArrayList();
int valid = 0;
for (int i = 0; i < subset.length; i++) {
IContentDescriber describer = ((ContentType) subset[i]).getDescriber();
int status = IContentDescriber.INDETERMINATE;
if (describer != null) {
status = describe(describer, buffer, null, 0);
if (status == IContentDescriber.INVALID)
continue;
}
if (status == IContentDescriber.VALID)
appropriate.add(valid++, subset[i]);
else
appropriate.add(subset[i]);
}
return (IContentType[]) appropriate.toArray(new IContentType[appropriate.size()]);
}
ContentType internalGetContentType(String contentTypeIdentifier) {
return (ContentType) catalog.get(contentTypeIdentifier);
}
private boolean isBaseTypeOf(IContentType base, IContentType target) {
IContentType targetBase = target.getBaseType();
if (targetBase == null)
return false;
return targetBase == base ? true : isBaseTypeOf(base, targetBase);
}
private void makeAliases(Map fileSpecs) {
// process all content types per file specs
// marking conflicting content types as aliases
// for the higher priority content type
for (Iterator i = fileSpecs.values().iterator(); i.hasNext();) {
Set associated = (Set) i.next();
if (associated.size() < 2)
// no conflicts here
continue;
Iterator j = associated.iterator();
ContentType ellected = (ContentType) j.next();
while (j.hasNext())
((ContentType) j.next()).setAliasTarget(ellected);
}
}
private int moreAppropriate(IContentType type1, IContentType type2) {
return isBaseTypeOf(type1, type2) ? -1 : (isBaseTypeOf(type2, type1) ? 1 : 0);
}
//TODO: should take any user-defined precedences into account
//TODO: should pick a common ancestor if two different specific types are deemed appropriate
private IContentType mostAppropriate(List candidates) {
int candidatesCount = candidates.size();
if (candidatesCount == 0)
return null;
IContentType chosen = (IContentType) candidates.remove(candidatesCount - 1);
if (candidatesCount == 1)
return chosen;
for (Iterator i = candidates.iterator(); i.hasNext();) {
IContentType current = (IContentType) i.next();
i.remove();
switch (moreAppropriate(current, chosen)) {
case -1 :
// currently chosen is more appropriate
break;
case 1 :
// a more appropriate content type has been found - elect it as the new chosen one
chosen = current;
continue;
default :
// two candidates are equally appropriate - cannot choose
return null;
}
}
return chosen;
}
protected void reorganize() {
fileExtensions.clear();
fileNames.clear();
// forget the validation status and aliases for all content types
for (Iterator i = catalog.values().iterator(); i.hasNext();) {
ContentType type = ((ContentType) i.next());
type.setValidation(ContentType.UNKNOWN);
type.setAliasTarget(null);
}
// do the validation
for (Iterator i = catalog.values().iterator(); i.hasNext();) {
ContentType type = (ContentType) i.next();
if (!ensureValid(type))
continue;
addFileSpecContributor(type, IContentType.FILE_EXTENSION_SPEC, fileExtensions);
addFileSpecContributor(type, IContentType.FILE_NAME_SPEC, fileNames);
}
makeAliases(fileNames);
makeAliases(fileExtensions);
}
protected void startup() {
catalog = new HashMap();
builder.startup();
builder.buildContentTypes();
}
/*
* Returns the extension for a file name (omiting the leading '.').
*/
static String getFileExtension(String fileName) {
int dotPosition = fileName.lastIndexOf('.');
return (dotPosition == -1 || dotPosition == fileName.length() - 1) ? null : fileName.substring(dotPosition + 1);
}
public synchronized static IContentTypeManager getInstance() {
if (instance != null)
return instance;
instance = new ContentTypeManager();
instance.startup();
return instance;
}
/*
* Returns null if no bytes can be read
*/
static ByteArrayInputStream readBuffer(InputStream contents) throws IOException {
boolean failed = false;
try {
contents.mark(MARK_LIMIT);
byte[] buffer = new byte[MARK_LIMIT];
int read = contents.read(buffer);
return read == -1 ? null : new ByteArrayInputStream(buffer, 0, read);
} catch (IOException ioe) {
failed = true;
throw ioe;
} finally {
if (!failed)
contents.reset();
}
}
}