/******************************************************************************* * Copyright (c) 2006, 2013 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.debug.internal.ui.views.variables.details; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.StringTokenizer; import org.eclipse.core.expressions.EvaluationResult; import org.eclipse.core.expressions.Expression; import org.eclipse.core.expressions.ExpressionConverter; import org.eclipse.core.expressions.ExpressionTagNames; import org.eclipse.core.expressions.IEvaluationContext; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.internal.core.IConfigurationElementConstants; import org.eclipse.debug.internal.ui.DebugUIPlugin; import org.eclipse.debug.ui.IDebugUIConstants; import org.eclipse.debug.ui.IDetailPane; import org.eclipse.debug.ui.IDetailPaneFactory; import org.eclipse.jface.viewers.IStructuredSelection; import org.osgi.service.prefs.BackingStoreException; /** * Organizes the detail factories contributed through the extension point and keeps * track of the detail panes the factories produce. Accessed as a singleton through * the getDefault() method. * * @see IDetailPaneFactory * @see IDetailPane * @since 3.3 */ public class DetailPaneManager { /** * Acts as a proxy between the detail pane manager and the factories contributed * to the extension point. Only loads information from the plug-in xml and only * instantiates the specified factory if required (lazy loading). */ private class DetailPaneFactoryExtension implements IDetailPaneFactory{ private IConfigurationElement fConfigElement; private IDetailPaneFactory fFactory; private Expression fEnablementExpression; public DetailPaneFactoryExtension(IConfigurationElement configElement){ fConfigElement = configElement; } /** * Instantiates the factory and asks it to produce the IDetailPane for * the given ID * @param paneID the identifier of the detail pane to create * @return the new detail pane or null if the backing {@link IDetailPaneFactory} is null */ @Override public IDetailPane createDetailPane(String paneID){ if (getFactory() != null){ return getFactory().createDetailPane(paneID); } return null; } /** * Instantiates the factory and asks it for the set of detail pane * IDs that the factory can produce for the given selection. * @param selection the current view selection * @return the set of detail pane type for the given selection or an empty set, never null */ @Override public Set getDetailPaneTypes(IStructuredSelection selection) { if (getFactory() != null){ return getFactory().getDetailPaneTypes(selection); } return Collections.EMPTY_SET; } /** * Instantiates the factory and asks it for the detail pane ID * that the factory considers the default for the given selection. * @param selection the current view selection * @return the identifier of the default detail pane or null if the backing {@link IDetailPaneFactory} is null */ @Override public String getDefaultDetailPane(IStructuredSelection selection) { if (getFactory() != null){ return getFactory().getDefaultDetailPane(selection); } return null; } /** * Instantiates the factory and asks it to produce the name of the detail pane * for the given ID. * @param paneID the detail pane identifier * @return the name of the detail pane or null if the backing {@link IDetailPaneFactory} is null */ @Override public String getDetailPaneName(String paneID) { if (getFactory() != null){ return getFactory().getDetailPaneName(paneID); } return null; } /** * Instantiates the factory and asks it to produce the description of the * detail pane for the given ID. * @param paneID the detail pane identifier * @return the description of the detail pane or null if the backing {@link IDetailPaneFactory} is null */ @Override public String getDetailPaneDescription(String paneID) { if (getFactory() != null){ return getFactory().getDetailPaneDescription(paneID); } return null; } /** * Returns the instantiated factory specified by the class property. * @return the singleton {@link IDetailPaneFactory} */ private IDetailPaneFactory getFactory(){ if (fFactory != null) { return fFactory; } try{ Object obj = fConfigElement.createExecutableExtension(IConfigurationElementConstants.CLASS); if(obj instanceof IDetailPaneFactory) { fFactory = (IDetailPaneFactory)obj; } else { throw new CoreException(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), IDebugUIConstants.INTERNAL_ERROR, "org.eclipse.debug.ui.detailFactories extension failed to load a detail factory because the specified class does not implement org.eclipse.debug.ui.IDetailPaneFactory. Class specified was: " + obj, null)); //$NON-NLS-1$ } } catch (CoreException e){ DebugUIPlugin.log(e.getStatus()); fFactory = null; } return fFactory; } /** * Checks if the enablement expression for the factory evaluates to true for the * given selection. * @param selection the current view selection * @return true if the backing {@link IDetailPaneFactory} applies to the given selection, false otherwise */ public boolean isEnabled(IStructuredSelection selection) { boolean enabled = false; // Only the default factory should be enabled for null selections if (selection == null || selection.isEmpty()){ return "org.eclipse.debug.ui.defaultDetailPaneFactory".equals(fConfigElement.getAttribute(IConfigurationElementConstants.ID)); //$NON-NLS-1$ } Expression expression = getEnablementExpression(); if (expression != null) { List list = selection.toList(); IEvaluationContext context = DebugUIPlugin.createEvaluationContext(list); context.addVariable("selection", list); //$NON-NLS-1$ enabled = evalEnablementExpression(context, expression); } else { enabled = true; } return enabled; } /** * Evaluate the given expression within the given context and return * the result. Returns true iff result is either TRUE or NOT_LOADED. * This allows optimistic inclusion of shortcuts before plug-ins are loaded. * Returns false if expression is null. * * @param exp the enablement expression to evaluate or null * @param context the context of the evaluation. Usually, the * user's selection. * @return the result of evaluating the expression */ private boolean evalEnablementExpression(IEvaluationContext context, Expression exp) { try{ if (exp != null){ EvaluationResult result = exp.evaluate(context); if (result == EvaluationResult.TRUE || result == EvaluationResult.NOT_LOADED){ return true; } } } catch (CoreException e){ DebugUIPlugin.log(e.getStatus()); } return false; } /** * Returns an expression that represents the enablement logic for the * detail pane factory or null if none. * * @return an evaluatable expression or null */ private Expression getEnablementExpression(){ // all of this stuff is optional, so...tedious testing is required if (fEnablementExpression == null) { try{ IConfigurationElement[] elements = fConfigElement.getChildren(ExpressionTagNames.ENABLEMENT); IConfigurationElement enablement = elements.length > 0 ? elements[0] : null; if (enablement != null) { fEnablementExpression = ExpressionConverter.getDefault().perform(enablement); } } catch (CoreException e){ DebugUIPlugin.log(e.getStatus()); fEnablementExpression = null; } } return fEnablementExpression; } } /** * There should only ever be once instance of this manager for the workbench. */ private static DetailPaneManager fgSingleton; /** * Maps the IDs of types of detail panes to the factory that can create them. * There can currently only be one factory for a given type of details pane. */ private Map fFactoriesByPaneID; /** * Maps a Set of detail pane id's to the one detail pane id that is preferred. */ private Map, String> fPreferredDetailPanes; /** * The set of all factories that have been loaded from the extension point. */ private List fKnownFactories; /** * Preference key for storing the preferred detail panes map. * @see #storePreferredDetailsAreas() * @see #loadPreferredDetailsAreas() */ public static final String PREF_DETAIL_AREAS = "preferredDetailPanes"; //$NON-NLS-1$ private DetailPaneManager(){ fFactoriesByPaneID = new HashMap<>(); fFactoriesByPaneID.put(MessageDetailPane.ID, new DefaultDetailPaneFactory()); } public static DetailPaneManager getDefault(){ if (fgSingleton == null) { fgSingleton = new DetailPaneManager(); } return fgSingleton; } /** * Returns the ID of the preferred detail pane for the given selection. * * @param selection The selection to display in the detail pane * @return The ID of the preferred detail pane or null */ public String getPreferredPaneFromSelection(IStructuredSelection selection){ List possibleFactories = getEnabledFactories(selection); Set possiblePaneIDs = getPossiblePaneIDs(possibleFactories, selection); return chooseDetailsAreaIDInSet(possiblePaneIDs, possibleFactories, selection); } /** * Returns the set of all possible detail panes the can display the given * selection. * * @param selection The selection to display in the detail pane * @return The set of IDs of all possible detail panes for the given selection */ public Set getAvailablePaneIDs(IStructuredSelection selection) { List possibleFactories = getEnabledFactories(selection); return getPossiblePaneIDs(possibleFactories, selection); } /** * Given the ID of a details pane, this method will try to find the factory * that creates it and return an instantiation of that area. *

* This method will not call the init() method of the IDetailsPane. *

* * @param ID The ID of the requested pane * @return The instantiated pane or null */ public IDetailPane getDetailPaneFromID(String ID){ IDetailPaneFactory factory = fFactoriesByPaneID.get(ID); if (factory != null){ return factory.createDetailPane(ID); } return null; } /** * Given the ID of a details pane, this method will try to find the factory * that creates it and ask it for the name of the details pane. * * @param ID The ID of the requested pane * @return The name of the details pane or null */ public String getNameFromID(String ID){ IDetailPaneFactory factory = fFactoriesByPaneID.get(ID); if (factory != null){ return factory.getDetailPaneName(ID); } return null; } /** * Given the ID of a details pane, this method will try to find the factory * that creates it and ask it for the description of the details pane. * * @param ID The ID of the requested pane * @return The description of the details pane or null */ public String getDescriptionFromID(String ID){ IDetailPaneFactory factory = fFactoriesByPaneID.get(ID); if (factory != null){ return factory.getDetailPaneDescription(ID); } return null; } /** * Returns the set of IDetailPaneFactories (they will be DetailPaneFactoryDelegates) that were * contributed to the extension point and are enabled for the given selection * (enabled if the factory does not have an enablement expression or if the * enablement expression evaluates to true). * @param selection the current view selection * * @return The factories enabled for the selection or an empty collection. */ private List getEnabledFactories(IStructuredSelection selection) { List factoriesForSelection = new ArrayList<>(); if (fKnownFactories == null) { initializeDetailFactories(); } for (IDetailPaneFactory currentFactory : fKnownFactories) { if (currentFactory instanceof DetailPaneFactoryExtension){ if (((DetailPaneFactoryExtension)currentFactory).isEnabled(selection)){ factoriesForSelection.add(currentFactory); } } } return factoriesForSelection; } /** * Produces the set of IDs for all possible detail panes that can be used to display * the given selection. * * @param factoriesToQuery The collection of factories to check * @param selection The selection to be displayed * @return Set of pane IDs or an empty set */ private Set getPossiblePaneIDs(List factoriesToQuery, IStructuredSelection selection) { Set idsForSelection = new LinkedHashSet<>(); for (IDetailPaneFactory currentFactory : factoriesToQuery) { for (String currentAreaTypeID : currentFactory.getDetailPaneTypes(selection)) { fFactoriesByPaneID.put(currentAreaTypeID, currentFactory); idsForSelection.add(currentAreaTypeID); } } return idsForSelection; } /** * Given a set of possible detail pane IDs, this method will determine which pane is * preferred and should be used to display the selection. This method chooses a pane * by storing previous choices and can be set using a context menu. * * @param possiblePaneIDs The set of possible detail pane IDs * @param enabledFactories the complete listing of enable {@link IDetailPaneFactory}s * @param selection the current selection from the variables view * @return The preferred detail pane ID or null */ private String chooseDetailsAreaIDInSet(Set possiblePaneIDs, List enabledFactories, IStructuredSelection selection) { if (possiblePaneIDs == null || possiblePaneIDs.isEmpty()){ return null; } String preferredID = getUserPreferredDetailPane(possiblePaneIDs); if (preferredID == null){ // If there is no preferred pane already set, check the factories to see there is a default pane for (IDetailPaneFactory currentFactory : enabledFactories) { preferredID = currentFactory.getDefaultDetailPane(selection); if (preferredID != null) { break; } } // If the factories don't have a default, try to choose the DefaultDetailPane if (preferredID == null){ Iterator paneIterator = possiblePaneIDs.iterator(); // If the DefaultDetailPane is not in the set, just use the first in the set preferredID = paneIterator.next(); while (paneIterator.hasNext() && preferredID != DefaultDetailPaneFactory.DEFAULT_DETAIL_PANE_ID) { String currentID = paneIterator.next(); if (currentID.equals(DefaultDetailPaneFactory.DEFAULT_DETAIL_PANE_ID)){ preferredID = currentID; } } } setPreferredDetailPane(possiblePaneIDs, preferredID); } return preferredID; } /** * Initializes the collection of known factories from extension point contributions. */ private synchronized void initializeDetailFactories(){ if (fKnownFactories == null){ fKnownFactories = new ArrayList<>(); IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(DebugUIPlugin.getUniqueIdentifier(), IDebugUIConstants.EXTENSION_POINT_DETAIL_FACTORIES); IConfigurationElement[] infos = extensionPoint.getConfigurationElements(); DetailPaneFactoryExtension delegate = null; for(int i = 0; i < infos.length; i++) { delegate = new DetailPaneFactoryExtension(infos[i]); fKnownFactories.add(delegate); } } } /** * Returns the preferred pane ID from the given set if the mapping has been set. * * @param possibleDetailsAreaIDs Set of possible pane IDs * @return The preferred ID or null */ public String getUserPreferredDetailPane(Set possibleDetailsAreaIDs) { if (fPreferredDetailPanes == null){ loadPreferredDetailsAreas(); } return fPreferredDetailPanes.get(possibleDetailsAreaIDs); } /** * Adds or updates the mapping to set which pane ID is preferred for a certain * set of possible IDs. * * @param possibleDetailsAreaIDs The set of possible IDs * @param preferredDetailsAreaID The preferred ID in the set. */ public void setPreferredDetailPane(Set possibleDetailsAreaIDs, String preferredDetailsAreaID) { if (possibleDetailsAreaIDs == null) { return; } if (fPreferredDetailPanes == null){ loadPreferredDetailsAreas(); } String currentKey = fPreferredDetailPanes.get(possibleDetailsAreaIDs); if (currentKey == null || !currentKey.equals(preferredDetailsAreaID)){ fPreferredDetailPanes.put(possibleDetailsAreaIDs, preferredDetailsAreaID); storePreferredDetailsAreas(); } } /** * Stores the map of preferred detail pane IDs to the preference store in the format: * * Key1A,Key1B:Value1|Key2A,Key2B,Key2C:Value2| * * Where the sub keys (Key1A, Key1B, etc.) are the elements of the set used at the * key in the mapping and the values are the associated String value in the mapping. */ private void storePreferredDetailsAreas() { StringBuffer buffer= new StringBuffer(); for (Entry, String> entry : fPreferredDetailPanes.entrySet()) { for (String currentID : entry.getKey()) { buffer.append(currentID); buffer.append(','); } buffer.deleteCharAt(buffer.length()-1); buffer.append(':'); buffer.append(entry.getValue()); buffer.append('|'); } IEclipsePreferences node = InstanceScope.INSTANCE.getNode(DebugUIPlugin.getUniqueIdentifier()); if(node != null) { node.put(PREF_DETAIL_AREAS, buffer.toString()); try { node.flush(); } catch (BackingStoreException e) { DebugUIPlugin.log(e); } } } /** * Loads the map of preferred detail pane IDs from the preference store. * * @see #storePreferredDetailsAreas() */ private void loadPreferredDetailsAreas() { fPreferredDetailPanes = new HashMap<>(); String preferenceValue = Platform.getPreferencesService().getString(DebugUIPlugin.getUniqueIdentifier(), PREF_DETAIL_AREAS, "", //$NON-NLS-1$ null); StringTokenizer entryTokenizer = new StringTokenizer(preferenceValue,"|"); //$NON-NLS-1$ while (entryTokenizer.hasMoreTokens()){ String token = entryTokenizer.nextToken(); int valueStart = token.indexOf(':'); StringTokenizer keyTokenizer = new StringTokenizer(token.substring(0,valueStart),","); //$NON-NLS-1$ Set keys = new LinkedHashSet<>(); while (keyTokenizer.hasMoreTokens()){ keys.add(keyTokenizer.nextToken()); } fPreferredDetailPanes.put(keys, token.substring(valueStart+1)); } } }