/******************************************************************************* * 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; } } }