/*******************************************************************************
* Copyright (c) 2007 IBM Corporation.
* 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:
* Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation
*******************************************************************************/
package org.eclipse.imp.language;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.imp.core.ErrorHandler;
import org.eclipse.imp.editor.EditorInputUtils;
import org.eclipse.imp.editor.UniversalEditor;
import org.eclipse.imp.preferences.PreferenceCache;
import org.eclipse.imp.runtime.RuntimePlugin;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.IDocument;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorMapping;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.registry.EditorDescriptor;
import org.eclipse.ui.internal.registry.EditorRegistry;
import org.eclipse.ui.internal.registry.FileEditorMapping;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.osgi.framework.Bundle;
/**
* @author Claffra
* @author rfuhrer@watson.ibm.com
* @author jurgen@vinju.org
* @author Stan Sutton (suttons@us.ibm.com)
*
* Registry for IMP language contributors.
*/
@SuppressWarnings("restriction")
public class LanguageRegistry {
private static Object sStatusCheckMutex = new Object();
private static boolean sIsFullyInitialized = false;
private static Map<String, Language> sRegister;
private static IEditorDescriptor sUniversalEditor;
private static EditorRegistry sEditorRegistry;
/*
* No one should instantiate this class.
* TODO: Perhaps should be a singleton class.
*/
private LanguageRegistry() {}
private static Map<String, Language> getRegister() {
if(sRegister == null) {
sRegister = new HashMap<String, Language>();
}
return sRegister;
}
/**
* Initialize the registry. Discover all contributors to the
* languageDescription extension point. The registry will not be fully
* initialized until the registerLanguages() method has been called.
*/
private static void preInitEditorRegistry() {
try {
sEditorRegistry = (EditorRegistry) PlatformUI.getWorkbench().getEditorRegistry();
initializeUniversalEditorDescriptor(sEditorRegistry);
IExtensionPoint extensionPoint =
Platform.getExtensionRegistry().getExtensionPoint(RuntimePlugin.IMP_RUNTIME, ServiceFactory.LANGUAGE_DESCRIPTION_POINT_ID);
if (extensionPoint == null) {
ErrorHandler.reportError("IMP language descriptor extension point '" +
(RuntimePlugin.IMP_RUNTIME + "." + ServiceFactory.LANGUAGE_DESCRIPTION_POINT_ID) +
"' non-existent?");
} else {
IConfigurationElement[] elements = extensionPoint.getConfigurationElements();
if (elements != null) {
for (IConfigurationElement element : elements) {
Bundle bundle = Platform.getBundle(element.getDeclaringExtension().getNamespaceIdentifier());
if (bundle != null) {
register(new Language(element));
}
}
}
}
} catch (InvalidRegistryObjectException e) {
if (PreferenceCache.emitMessages) {
RuntimePlugin.getInstance().logException("IMP LanguageRegistry error in preInitEditorRegistry()", e);
} else {
ErrorHandler.reportError("IMP LanguageRegistry error", e);
}
}
}
/**
* Returns the language description for a given editor input. First the file
* extension is used to discover registered languages. Then each language is
* used to ensure it actually supports the content of the file.
*
* @param editorInput
* the editorInput to be opened
* @return the contributed language description
* @return null if no language is contributed with the given
* extension/content
*/
public static Language findLanguage(IEditorInput editorInput, IDocumentProvider docProvider) {
if (!sIsFullyInitialized)
initializeRegistryAsNeeded();
IPath path= EditorInputUtils.getPath(editorInput);
return findLanguage(path, docProvider.getDocument(editorInput));
}
/**
* Determine the source language contained by the resource at the given path.
*
* @param path
* @param doc if non-null, may be used to validate the contents of the document
* (e.g. to distinguish dialects)
* @return
*/
public static Language findLanguage(IPath path, IDocument doc) {
// TODO: use Eclipse content type instead
if (!isFullyInitialized())
initializeRegistryAsNeeded();
String extension= path.getFileExtension();
String docContents= (doc != null) ? doc.get() : null;
// N.B. It's ok for multiple language descriptors to specify the same
// file name extension; the associated validators should use the file
// contents to identify the dialects.
if (extension != null) {
for (Language lang : getRegister().values()) {
if (lang.hasExtension(extension)) {
LanguageValidator validator = lang.getValidator();
if (validator != null && docContents != null) {
if (validator.validate(docContents)) {
return lang;
}
} else {
return lang;
}
}
}
}
if (PreferenceCache.emitMessages) {
RuntimePlugin.getInstance().writeErrorMsg("No language support for text/source file of type '" +
extension + "'.");
} else {
ErrorHandler.reportError("No language support for text/source file of type '" +
extension + "'.");
}
return null;
}
public static Language findLanguageByNature(String natureID) {
if (!isFullyInitialized())
initializeRegistryAsNeeded();
for (Language lang : getRegister().values()) {
String aNatureID = lang.getNatureID();
if (aNatureID != null && aNatureID.equals(natureID)) {
return lang;
}
}
return null;
}
public static Collection<Language> getLanguages() {
if (!isFullyInitialized())
initializeRegistryAsNeeded();
return Collections.unmodifiableCollection(getRegister().values());
}
public static Language findLanguage(String languageName) {
if (!isFullyInitialized())
initializeRegistryAsNeeded();
return getRegister().get(languageName.toLowerCase());
}
/**
* Registers a language dynamically (as opposed to using an extension
* point). This binds the file extensions associated with the language to
* the IMP Universal editor.
*
* @param language
*/
public static void registerLanguage(Language language) {
if (!isFullyInitialized())
initializeRegistryAsNeeded();
register(language);
List<IFileEditorMapping> mappings = new ArrayList<IFileEditorMapping>();
Collections.addAll(mappings, getEditorRegistry().getFileEditorMappings());
addUniversalEditorMappings(language.getName(), language.getIconPath(), language.getFilenameExtensions(), language.getBundleID(), mappings);
updateEditorRegistry(mappings);
}
private static EditorRegistry getEditorRegistry() {
return sEditorRegistry;
}
/**
* Removes stale registrations and then registers all languages declared
* using the IMP languageDescription extension point. The file extensions of
* all registered languages are bound to the IMP universal editor.
*
* This method is called at earlyStartup time to initialize the registry.
* It is also called from within UniversalEditor.createPartControl(..)
* (probably as a way to make sure that the registry is initialized if it
* hasn't been already). It is also called from various accessor methods
* of the LanguageRegistry class to similarly assure that the registry is
* initialized.
*
* To prevent thread access errors and other possible concurrency conflicts,
* this method executes within a synchronized block. It both tests and sets
* isFullyInitialized within this block, as a means to assure that the registry
* will be initialized serially and only once. Although isFullyInitialized
* can be tested from anywhere, this is the only place that it should be set.
*/
public static void initializeRegistryAsNeeded() {
synchronized(sStatusCheckMutex) {
if(isFullyInitialized()) {
return;
}
preInitEditorRegistry();
if (PreferenceCache.emitMessages) {
RuntimePlugin.getInstance().writeInfoMsg(
"Looking for IMP language description extensions...");
}
// List<String> langExtens= collectAllLanguageFileNameExtensions();
List<IFileEditorMapping> newMap = new ArrayList<IFileEditorMapping>();
addNonUniversalEditorMappings(newMap);
for(Language lang : getRegister().values()) {
addUniversalEditorMappings(lang.getName(), lang.getIconPath(), lang.getFilenameExtensions(), lang.getBundleID(), newMap);
}
updateEditorRegistry(newMap);
setFullyInitialized();
}
}
private static void addNonUniversalEditorMappings(List<IFileEditorMapping> newMap) {
for (IFileEditorMapping mapping : getEditorRegistry().getFileEditorMappings()) {
IEditorDescriptor defaultEditor = mapping.getDefaultEditor();
if (defaultEditor == null
|| !defaultEditor.getId().equals(UniversalEditor.EDITOR_ID)) {
newMap.add(mapping);
}
}
}
public static class BundleImageDescriptor extends ImageDescriptor {
private final Bundle bundle;
private final String iconPath;
private final String langName;
public BundleImageDescriptor(String iconPath, Bundle bundle, String langName) {
this.langName= langName;
this.bundle= bundle;
this.iconPath= iconPath;
}
@Override
public ImageData getImageData() {
InputStream in = getStream();
ImageData result = null;
if (in != null) {
try {
result = new ImageData(in);
} catch (SWTException e) {
if (e.code != SWT.ERROR_INVALID_IMAGE) {
throw e;
// fall through otherwise
}
} finally {
try {
in.close();
} catch (IOException e) {
//System.err.println(getClass().getName()+".getImageData(): "+
// "Exception while closing InputStream : "+e);
}
}
}
return result;
}
private InputStream getStream() {
InputStream is = null;
try {
if (this.iconPath != null) {
is = bundle.getResource(iconPath).openStream();
}
} catch (IOException e) {
RuntimePlugin.getInstance().logException("Unable to find icon for language " + langName, e);
return null;
}
if (is == null) {
return null;
} else {
return new BufferedInputStream(is);
}
}
}
private static class IMPFileEditorMapping extends FileEditorMapping {
private ImageDescriptor fImageDescriptor;
private IEditorDescriptor fEditor;
public IMPFileEditorMapping(final String langName, String extension, final String iconPath, String bundleID) {
super(extension);
final Bundle bundle= Platform.getBundle(bundleID);
fImageDescriptor= new BundleImageDescriptor(iconPath, bundle, langName);
}
public void setTheDefaultEditor(IEditorDescriptor editor) {
fEditor= editor;
}
@Override
public IEditorDescriptor getDefaultEditor() {
return fEditor;
}
@Override
public ImageDescriptor getImageDescriptor() {
return fImageDescriptor;
}
}
/**
* Adds new mappings to the universal editor for a set of extensions
* @param extensions
* @param newMap
*/
private static void addUniversalEditorMappings(String langName, String langIcon,
Iterable<String> extensions, String bundleID, List<IFileEditorMapping> newMap) {
IFileEditorMapping[] mappings= sEditorRegistry.getFileEditorMappings();
// RMF 3/25/2009 - There doesn't seem to be a way to set the editor's label
// programmatically; (1) IEditorDescriptor doesn't expose enough API, (2) the
// EditorDescriptor ctor is not visible, (3) the class EditorDescriptor is final,
// (4) getEditorRegistry().setFileEditorMappings() will only accept an array
// of FileEditorMappings, and (5) FileEditorMapping requires an EditorDescriptor.
for (String ext : extensions) {
IFileEditorMapping mapping= findMappingFor(ext, mappings);
if (mapping == null || (mapping != null && mapping.getDefaultEditor() != null && mapping.getDefaultEditor().getId().equals(sUniversalEditor.getId()))) {
// Replace the file editor mapping even if it already pointed to the universal editor,
// since the persisted association turns into a FileEditorMapping when re-read, thus
// losing the icon (which FileEditorMapping gets from the IEditorDescriptor).
mapping= new IMPFileEditorMapping(langName, ext, langIcon, bundleID);
}
IEditorDescriptor defaultEditor= mapping.getDefaultEditor();
FileEditorMapping fem= (FileEditorMapping) mapping;
if (defaultEditor == null || defaultEditor.getId().equals("")) {
fem.setDefaultEditor((EditorDescriptor) sUniversalEditor);
} else {
// SMS 19 Nov 2008
// Revised else branch according to patch provided by Edward Willink
// Bug #242967, attachment id=109002
boolean gotIt = false;
for (IEditorDescriptor editor : fem.getEditors()) {
if (editor == sUniversalEditor) {
gotIt = true;
break;
}
}
if (!gotIt) {
// See whether there's an entry pointing to a UniversalEditor-derived class
for(IEditorDescriptor editor: fem.getEditors()) {
EditorDescriptor edDesc= (EditorDescriptor) editor;
String edClassName= edDesc.getClassName();
IConfigurationElement edElem= edDesc.getConfigurationElement();
Bundle edBundle= Platform.getBundle(edElem.getNamespaceIdentifier());
try {
Class<?> edClass= edBundle.loadClass(edClassName);
if (UniversalEditor.class.isAssignableFrom(edClass)) {
gotIt= true;
break;
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
if (!gotIt) {
fem.addEditor((EditorDescriptor) sUniversalEditor);
}
}
newMap.add(mapping);
}
}
private void addEditorIfNeeded(IMPFileEditorMapping fem, EditorDescriptor editor) {
// SMS 19 Nov 2008
// Revised else branch according to patch provided by Edward Willink
// Bug #242967, attachment id=109002
boolean gotIt = false;
for (IEditorDescriptor fileEditor : fem.getEditors()) {
if (fileEditor.getId().equals(editor.getId())) {
gotIt = true;
break;
}
}
if (!gotIt) {
fem.addEditor(editor);
}
}
private static IFileEditorMapping findMappingFor(String ext, IFileEditorMapping[] mappings) {
for(int i= 0; i < mappings.length; i++) {
if (mappings[i].getExtension().equals(ext)) {
return mappings[i];
}
}
return null;
}
/**
* Commits a new list of editor mappings to the editorRegistry
* @param newMap
*/
private static void updateEditorRegistry(final List<IFileEditorMapping> newMap) {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
getEditorRegistry().setFileEditorMappings(newMap.toArray(new FileEditorMapping[newMap.size()]));
// TODO Do we really want to save the associations persistently?
getEditorRegistry().saveAssociations();
}
});
}
private static List<String> collectAllLanguageFileNameExtensions() {
List<String> allExtens = new ArrayList<String>(getRegister().size());
for (Language lang : getRegister().values()) {
allExtens.addAll(lang.getFilenameExtensions());
}
return allExtens;
}
private static void initializeUniversalEditorDescriptor(EditorRegistry editorRegistry) {
final IEditorDescriptor[] allEditors = editorRegistry.getSortedEditorsFromPlugins();
for (IEditorDescriptor editor : allEditors) {
if (editor.getId().equals(UniversalEditor.EDITOR_ID)) {
sUniversalEditor = editor;
if (PreferenceCache.emitMessages) {
RuntimePlugin.getInstance().writeInfoMsg(
"Universal editor descriptor: " +
sUniversalEditor.getId() + ":" +
sUniversalEditor.getLabel());
}
return;
}
}
if (sUniversalEditor == null) {
if (PreferenceCache.emitMessages) {
RuntimePlugin.getInstance().writeErrorMsg(
"IMP LanguageRegistry error in initializeUniversalEditorDescroptor(): unable to initialize UniversalEditor");
} else {
ErrorHandler.reportError(
"Unable to locate Universal Editor descriptor", null);
}
}
}
private static void register(Language language) {
getRegister().put(language.getName().toLowerCase(), language);
if (PreferenceCache.emitMessages) {
RuntimePlugin.getInstance().writeInfoMsg(
"Registered language description: " + language.getName());
}
}
private static void setFullyInitialized() {
sIsFullyInitialized = true;
}
private static boolean isFullyInitialized() {
return sIsFullyInitialized;
}
}