| /******************************************************************************* |
| * Copyright (c) 2010 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.wst.sse.ui.internal.contentassist; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtensionRegistry; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.InvalidRegistryObjectException; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.content.IContentType; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.wst.sse.ui.contentassist.AutoActivationDelegate; |
| import org.eclipse.wst.sse.ui.internal.Logger; |
| import org.eclipse.wst.sse.ui.internal.SSEUIPlugin; |
| |
| |
| /** |
| * <p>A registry for all extensions to the |
| * <code>org.eclipse.wst.sse.ui.completionProposal</code> |
| * extension point.</p> |
| */ |
| public final class CompletionProposalComputerRegistry { |
| |
| /** The extension schema name of the extension point */ |
| private static final String EXTENSION_POINT = "completionProposal"; //$NON-NLS-1$ |
| |
| /** The extension schema name of proposal category child elements. */ |
| private static final String ELEM_PROPOSAL_CATEGORY = "proposalCategory"; //$NON-NLS-1$ |
| |
| /** The extension schema name of proposal computer child elements. */ |
| private static final String ELEM_PROPOSAL_COMPUTER = "proposalComputer"; //$NON-NLS-1$ |
| |
| /** The extension schema name of proposal computer activation child elements. */ |
| private static final String ELEM_PROPOSAL_COMPUTER_EXTENDED_ACTIVATION = "proposalComputerExtendedActivation"; //$NON-NLS-1$ |
| |
| /** The extension schema name for element ID attributes */ |
| private static final String ATTR_ID= "id"; //$NON-NLS-1$ |
| |
| /** The extension schema name for the partition's auto-activation class */ |
| private static final String ATTR_AUTO_ACTIVATION_CLASS = "autoActivationDelegate"; //$NON-NLS-1$ |
| |
| /** preference key to keep track of the last known number of content assist computers */ |
| private static final String NUM_COMPUTERS_PREF_KEY = "content_assist_number_of_computers"; //$NON-NLS-1$ |
| |
| /** |
| * A fake partition type ID stating used to say a {@link CompletionProposalComputerDescriptor} should |
| * be associated with all partition types in a given content type. |
| */ |
| private static final String ALL_PARTITION_TYPES_ID = "all_partition_types_fake_ID"; //$NON-NLS-1$ |
| |
| /** State where in the registry has not yet been loaded */ |
| private static final byte NONE = 0; |
| |
| /** State where the registry is being initialized */ |
| private static final byte INITIALIZING = 1; |
| |
| /** State where in the registry is currently being loaded */ |
| private static final byte LOADING = 2; |
| |
| /** State where in the registry has been loaded */ |
| private static final byte LOADED = 3; |
| |
| /** The singleton instance. */ |
| private static CompletionProposalComputerRegistry fgSingleton= null; |
| |
| /** |
| * @return the singleton instance of the registry |
| */ |
| public static synchronized CompletionProposalComputerRegistry getDefault() { |
| if (fgSingleton == null) { |
| fgSingleton= new CompletionProposalComputerRegistry(); |
| } |
| |
| return fgSingleton; |
| } |
| |
| /** |
| * <code>{@link Map}<{@link String}, {@link CompletionProposalContentTypeContext}></code> |
| * <ul> |
| * <li><b>key:</b> content type ID</li> |
| * <li><b>value:</b> the context for the associated content type ID</li> |
| * <ul> |
| */ |
| private final Map fActivationContexts; |
| |
| /** |
| * <code>{@link Map}<{@link String}, {@link CompletionProposalComputerDescriptor}></code> |
| * <ul> |
| * <li><b>key:</b> descriptor ID</li> |
| * <li><b>value:</b> descriptor</li> |
| * <ul> |
| */ |
| private final Map fDescriptors = new HashMap(); |
| |
| /** A map maintaining the relationship between content types, partition types and their associated {@link AutoActivationDelegate}s*/ |
| private Map fAutoActivators = new HashMap(); |
| |
| /** The {@link CompletionProposalCategory}s tracked by this registry */ |
| private final List fCategories = new ArrayList(); |
| |
| /** Unmodifiable public list of the {@link CompletionProposalCategory}s tracked by this registry */ |
| private final List fPublicCategories = Collections.unmodifiableList(fCategories); |
| |
| /** |
| * <p>The current state of the registry</p> |
| * |
| * @see #NONE |
| * @see #LOADING |
| * @see #LOADED |
| */ |
| private byte fState; |
| |
| /** |
| * Lock that is held while loading, operations waiting for |
| * load to finish can lock on this as well |
| */ |
| private final Object fLoadingLock; |
| |
| /** <code>true</code> if computers have been uninstalled since last load */ |
| private boolean fHasUninstalledComputers= false; |
| |
| /** |
| * Creates a new instance. |
| */ |
| private CompletionProposalComputerRegistry() { |
| this.fActivationContexts = new HashMap(); |
| this.fState = CompletionProposalComputerRegistry.NONE; |
| this.fLoadingLock = new Object(); |
| } |
| |
| /** |
| * <p>Calling this when the registry has not yet been loaded will start a |
| * {@link Thread} to start loading the registry. All other operations on |
| * the registry will be blocked until the loading has completed. If |
| * the registry has already been loaded or is loading then this |
| * method does nothing.</p> |
| */ |
| public synchronized void initialize() { |
| if(fState == CompletionProposalComputerRegistry.NONE) { |
| fState = CompletionProposalComputerRegistry.INITIALIZING; |
| new Thread() { |
| public void run() { |
| CompletionProposalComputerRegistry.this.load(); |
| } |
| }.start(); |
| } |
| } |
| |
| /** |
| * <p><b>NOTE: </b>The returned list is read-only and is sorted in the order that the |
| * extensions were read in. There are no duplicate elements in the returned list. |
| * The returned list may change if plug-ins are loaded or unloaded while the |
| * application is running.</p> |
| * |
| * @return the list of proposal categories contributed to the |
| * <code>org.eclipse.wst.sse.ui.completionProposal</code> extension point (element type: |
| * {@link CompletionProposalCategory}) |
| */ |
| public List getProposalCategories() { |
| internalEnsureLoaded(); |
| return fPublicCategories; |
| } |
| |
| /** |
| * <p><b>NOTE: </b>The returned list is read-only and is sorted in the order that the |
| * extensions were read in. There are no duplicate elements in the returned list. |
| * The returned list may change if plug-ins are loaded or unloaded while the |
| * application is running.</p> |
| * |
| * @param contentTypeID get the {@link CompletionProposalCategory}s associated with this ID |
| * @return the {@link CompletionProposalCategory}s associated with the given content type ID |
| */ |
| public List getProposalCategories(String contentTypeID) { |
| internalEnsureLoaded(); |
| List result = new ArrayList(); |
| for(int i = 0; i < fCategories.size(); ++i) { |
| CompletionProposalCategory category = ((CompletionProposalCategory)fCategories.get(i)); |
| if(category.hasComputers(contentTypeID)) { |
| result.add(category); |
| } |
| } |
| |
| return Collections.unmodifiableList(result); |
| } |
| |
| /** |
| * @return <code>true</code> if the registry detected that computers got uninstalled since the last run |
| * <code>false</code> otherwise or if {@link #resetUnistalledComputers()} has been called |
| */ |
| public boolean hasUninstalledComputers() { |
| return fHasUninstalledComputers; |
| } |
| |
| /** |
| * <p>Clears the setting that uninstalled computers have been detected. |
| * This setting is used to decide weather a helpful message should be |
| * displayed to the user</p> |
| */ |
| public void resetUnistalledComputers() { |
| fHasUninstalledComputers = false; |
| } |
| |
| /** |
| * <p>Adds the given {@link CompletionProposalComputerDescriptor} to the registry.</p> |
| * |
| * @param contentTypeID the ID of the content type to associated the descriptor with |
| * @param partitionTypeID the ID of the partition type in the content type to associate |
| * the descriptor with, or <code>null</code> to associate with all partition types in |
| * the content type. |
| * @param descriptor the {@link CompletionProposalComputerDescriptor} to associate with |
| * the given content type and partition type |
| */ |
| void putDescription(String contentTypeID, String partitionTypeID, |
| CompletionProposalComputerDescriptor descriptor) { |
| |
| if(partitionTypeID == null) { |
| partitionTypeID = ALL_PARTITION_TYPES_ID; |
| } |
| |
| CompletionProposalContentTypeContext context = getContext(contentTypeID); |
| context.putDescriptor(partitionTypeID, descriptor); |
| } |
| |
| void putAutoActivator(String contentTypeID, String partitionTypeID, IConfigurationElement element) { |
| String autoActivationClass = element.getAttribute(ATTR_AUTO_ACTIVATION_CLASS); |
| if (autoActivationClass == null) |
| return; |
| |
| Map partitionMap = (Map) fAutoActivators.get(contentTypeID); |
| if (partitionMap == null) { |
| partitionMap = new HashMap(); |
| fAutoActivators.put(contentTypeID, partitionMap); |
| } |
| partitionMap.put(partitionTypeID, new Activator(element)); |
| } |
| |
| public AutoActivationDelegate getActivator(String contentTypeID, String partitionTypeID) { |
| Map partitionMap = (Map) fAutoActivators.get(contentTypeID); |
| if (partitionMap != null) { |
| Activator activator = (Activator) partitionMap.get(partitionTypeID); |
| if (activator != null) { |
| return activator.createAutoActivation(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param contentTypeID get only descriptors associated with this content type |
| * @param partitionTypeID get only descriptors associated with this partition type as well |
| * as describers associated with any partition type in the given content type |
| * @return all of the {@link CompletionProposalComputerDescriptor}s associated with the |
| * given content type and partition type (including any describers associated with all |
| * partition types in the given content type) |
| */ |
| List getProposalComputerDescriptors(String contentTypeID, String partitionTypeID) { |
| internalEnsureLoaded(); |
| |
| Set descriptorsSet = new HashSet(); |
| List contexts = this.getContexts(contentTypeID); |
| for(int i = 0; i < contexts.size(); ++i) { |
| CompletionProposalContentTypeContext contentSpecificContext = |
| (CompletionProposalContentTypeContext)contexts.get(i); |
| |
| //add all descriptors specific to the given content type and the given partition type |
| descriptorsSet.addAll(contentSpecificContext.getDescriptors(partitionTypeID)); |
| |
| //add all descriptors specific to the given content type but not specific to a partition type |
| descriptorsSet.addAll(contentSpecificContext.getDescriptors(ALL_PARTITION_TYPES_ID)); |
| } |
| |
| List descriptors = new ArrayList(descriptorsSet); |
| return descriptors != null ? Collections.unmodifiableList(descriptors) : Collections.EMPTY_LIST; |
| } |
| |
| /** |
| * @param contentTypeID get only descriptors associated with this content type |
| * @return all of the {@link CompletionProposalComputerDescriptor}s associated with the |
| * given content type |
| */ |
| List getProposalComputerDescriptors(String contentTypeID) { |
| internalEnsureLoaded(); |
| |
| Set descriptorsSet = new HashSet(); |
| |
| List contexts = this.getContexts(contentTypeID); |
| for(int i = 0; i < contexts.size(); ++i) { |
| CompletionProposalContentTypeContext contentSpecificContext = |
| (CompletionProposalContentTypeContext)contexts.get(i); |
| |
| //add all descriptors specific to the given content type |
| descriptorsSet.addAll(contentSpecificContext.getDescriptors()); |
| } |
| |
| List descriptors = new ArrayList(descriptorsSet); |
| return descriptors != null ? Collections.unmodifiableList(descriptors) : Collections.EMPTY_LIST; |
| } |
| |
| /** |
| * @return Unmodifiable list of all of the {@link CompletionProposalComputerDescriptor}s associated with |
| * this registry |
| */ |
| List getProposalComputerDescriptors() { |
| internalEnsureLoaded(); |
| return Collections.unmodifiableList( new ArrayList(fDescriptors.values())); |
| } |
| |
| /** |
| * <p>This method performs differently depending on the state of the registry</p> |
| * <ul> |
| * <li>{@link #NONE} - forces the registry to load now on the current thread</li> |
| * <li>{@link #LOADING} - blocks until the registry has finished loading on |
| * whichever thread it is being loaded on</li> |
| * <li>{@link #LOADED} - immediately returns</li> |
| * </ul> |
| */ |
| private void internalEnsureLoaded() { |
| switch (fState) { |
| case CompletionProposalComputerRegistry.NONE: |
| case CompletionProposalComputerRegistry.INITIALIZING: { |
| load(); |
| break; |
| } |
| case CompletionProposalComputerRegistry.LOADING: { |
| //as soon as this lock is released the loading process is done |
| synchronized (fLoadingLock) { |
| //sanity check |
| Assert.isTrue(fState == CompletionProposalComputerRegistry.LOADED, |
| "The state of the registry should be guaranteed to be LOADED " + //$NON-NLS-1$ |
| " once the loading lock has been released."); //$NON-NLS-1$ |
| } |
| break; |
| } |
| } |
| } |
| |
| /** |
| * <p>Loads the completion proposal extension points if they |
| * have not been loaded already</p> |
| */ |
| private void load() { |
| /* hold this lock while loading, others waiting for load to |
| * finish can synchronize on this lock to be guaranteed the load |
| * will be done when the lock is released |
| */ |
| synchronized (fLoadingLock) { |
| if(fState == CompletionProposalComputerRegistry.NONE || fState == CompletionProposalComputerRegistry.INITIALIZING) { |
| //update the state |
| fState = CompletionProposalComputerRegistry.LOADING; |
| |
| IExtensionRegistry registry= Platform.getExtensionRegistry(); |
| List extensionElements= new ArrayList(Arrays.asList(registry.getConfigurationElementsFor(SSEUIPlugin.ID, EXTENSION_POINT))); |
| |
| Map loadedDescriptors = new HashMap(); |
| List extendedComputerActivations = new ArrayList(); |
| |
| //get the categories and remove them from the extension elements |
| List categories= getCategories(extensionElements); |
| |
| //deal with the proposal computers and set aside the proposal computer activation extensions |
| for (Iterator iter= extensionElements.iterator(); iter.hasNext();) { |
| IConfigurationElement element= (IConfigurationElement) iter.next(); |
| try { |
| if (element.getName().equals(ELEM_PROPOSAL_COMPUTER)) { |
| //create the descriptor and add it to the registry |
| CompletionProposalComputerDescriptor desc = new CompletionProposalComputerDescriptor(element, categories); |
| desc.addToRegistry(); |
| loadedDescriptors.put(desc.getId(), desc); |
| } else if(element.getName().equals(ELEM_PROPOSAL_COMPUTER_EXTENDED_ACTIVATION)) { |
| extendedComputerActivations.add(element); |
| } |
| |
| } catch (InvalidRegistryObjectException x) { |
| /* |
| * Element is not valid any longer as the contributing plug-in was unloaded or for |
| * some other reason. Do not include the extension in the list and log it |
| */ |
| String message = "The extension ''" + element.toString() + "'' is invalid."; //$NON-NLS-1$ //$NON-NLS-2$ |
| IStatus status= new Status(IStatus.WARNING, SSEUIPlugin.ID, IStatus.OK, message, x); |
| Logger.log(status); |
| } catch (CoreException x) { |
| Logger.log(x.getStatus()); |
| } |
| } |
| |
| //deal with extended computer activations |
| for(int i = 0; i < extendedComputerActivations.size(); ++i) { |
| IConfigurationElement element = (IConfigurationElement)extendedComputerActivations.get(i); |
| String proposalComputerID = element.getAttribute(ATTR_ID); |
| CompletionProposalComputerDescriptor descriptor = |
| (CompletionProposalComputerDescriptor)loadedDescriptors.get(proposalComputerID); |
| if(descriptor != null) { |
| try { |
| //add the extra activation contexts to the registry |
| CompletionProposalComputerDescriptor.parseActivationAndAddToRegistry(element, descriptor); |
| } catch (InvalidRegistryObjectException x) { |
| /* |
| * Element is not valid any longer as the contributing plug-in was unloaded or for |
| * some other reason. Do not include the extension in the list and log it |
| */ |
| String message = "The extension ''" + element.toString() + "'' is invalid."; //$NON-NLS-1$ //$NON-NLS-2$ |
| IStatus status= new Status(IStatus.WARNING, SSEUIPlugin.ID, IStatus.OK, message, x); |
| Logger.log(status); |
| } catch (CoreException x) { |
| Logger.log(x.getStatus()); |
| } |
| |
| } else { |
| //activation extension has invalid computer ID |
| Logger.log(Logger.WARNING, "Configuration element " + element + //$NON-NLS-1$ |
| " intented to extend an existing completion proposal computer" + //$NON-NLS-1$ |
| " specified an invalid completion proposal computer ID " + //$NON-NLS-1$ |
| proposalComputerID); |
| } |
| } |
| |
| fCategories.clear(); |
| fCategories.addAll(categories); |
| |
| fDescriptors.clear(); |
| fDescriptors.putAll(loadedDescriptors); |
| |
| updateUninstalledComputerCount(); |
| |
| //update the state |
| fState = CompletionProposalComputerRegistry.LOADED; |
| } |
| } |
| } |
| |
| /** |
| * <p>Updates the uninstalled computer count</p> |
| */ |
| private void updateUninstalledComputerCount() { |
| IPreferenceStore preferenceStore = SSEUIPlugin.getDefault().getPreferenceStore(); |
| int lastNumberOfComputers= preferenceStore.getInt(NUM_COMPUTERS_PREF_KEY); |
| int currNumber= fDescriptors.size(); |
| fHasUninstalledComputers= lastNumberOfComputers > currNumber; |
| preferenceStore.putValue(NUM_COMPUTERS_PREF_KEY, Integer.toString(currNumber)); |
| } |
| |
| /** |
| * <p>Configures the categories found in the given {@link IConfigurationElement}s |
| * and removes them from the given list.</p> |
| * |
| * @param extensionElements {@link IConfigurationElement}s that include proposal |
| * category extensions |
| * @return {@link CompletionProposalCategory}s created from the given |
| * {@link IConfigurationElement}s that defined new proposal categories. |
| */ |
| private List getCategories(List extensionElements) { |
| List categories= new ArrayList(); |
| for (Iterator iter= extensionElements.iterator(); iter.hasNext();) { |
| IConfigurationElement element= (IConfigurationElement) iter.next(); |
| try { |
| if (element.getName().equals(ELEM_PROPOSAL_CATEGORY)) { |
| iter.remove(); // remove from list to leave only computers |
| |
| CompletionProposalCategory category= new CompletionProposalCategory(element); |
| categories.add(category); |
| } |
| } catch (InvalidRegistryObjectException x) { |
| /* Element is not valid any longer as the contributing plug-in was unloaded or for |
| * some other reason. Do not include the extension in the list and log it |
| */ |
| String message = "The extension ''" + element.toString() + "'' has become invalid."; //$NON-NLS-1$ //$NON-NLS-2$ |
| IStatus status= new Status(IStatus.WARNING, SSEUIPlugin.ID, IStatus.OK, message, x); |
| Logger.log(status); |
| } catch (CoreException x) { |
| Logger.log(x.getStatus()); |
| } |
| } |
| |
| return categories; |
| } |
| |
| /** |
| * <p>Gets the {@link CompletionProposalContentTypeContext} associated with the given content type, |
| * if one does not already exist then one is created</p> |
| * |
| * @param contentTypeID get the {@link CompletionProposalContentTypeContext} associated with this content type |
| * @return the existing or new {@link CompletionProposalContentTypeContext} associated with the given content type |
| */ |
| private CompletionProposalContentTypeContext getContext(String contentTypeID) { |
| CompletionProposalContentTypeContext context = (CompletionProposalContentTypeContext)this.fActivationContexts.get(contentTypeID); |
| if(context == null) { |
| context = new CompletionProposalContentTypeContext(contentTypeID); |
| this.fActivationContexts.put(contentTypeID, context); |
| } |
| |
| return context; |
| } |
| |
| /** |
| * <p>Gets all of the {@link CompletionProposalContentTypeContext}s associated with |
| * the given content type ID. A context is considered associated if its associated content type ID |
| * is either the given content type ID or is a base content type ID of the given content type ID.</p> |
| * |
| * @param contentTypeID get the contexts for this content type ID |
| * @return {@link List} of {@link CompletionProposalContentTypeContext}s associated with the given |
| * content type ID |
| */ |
| private List getContexts(String contentTypeID) { |
| List contexts = new ArrayList(); |
| IContentType contentType = Platform.getContentTypeManager().getContentType(contentTypeID); |
| |
| while(contentType != null) { |
| Object context = this.fActivationContexts.get(contentType.getId()); |
| if(context != null) { |
| contexts.add(context); |
| } |
| |
| contentType = contentType.getBaseType(); |
| } |
| |
| return contexts; |
| } |
| |
| private static class Activator { |
| IConfigurationElement fElement; |
| |
| public Activator(IConfigurationElement element) { |
| fElement = element; |
| } |
| |
| AutoActivationDelegate createAutoActivation() { |
| AutoActivationDelegate activation = null; |
| if (fElement != null) { |
| try { |
| activation = (AutoActivationDelegate) fElement.createExecutableExtension(ATTR_AUTO_ACTIVATION_CLASS); |
| } catch (CoreException e) { |
| } |
| } |
| return activation; |
| } |
| |
| } |
| } |