| /******************************************************************************* |
| * Copyright (c) 2006, 2018 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.ui.internal.navigator; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| import org.eclipse.core.runtime.Adapters; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.jface.viewers.IStructuredSelection; |
| import org.eclipse.jface.viewers.ITreeContentProvider; |
| import org.eclipse.jface.viewers.ITreePathContentProvider; |
| import org.eclipse.jface.viewers.ITreeSelection; |
| import org.eclipse.jface.viewers.StructuredViewer; |
| import org.eclipse.jface.viewers.TreePath; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.ISaveablesLifecycleListener; |
| import org.eclipse.ui.ISaveablesSource; |
| import org.eclipse.ui.Saveable; |
| import org.eclipse.ui.SaveablesLifecycleEvent; |
| import org.eclipse.ui.internal.navigator.VisibilityAssistant.VisibilityListener; |
| import org.eclipse.ui.internal.navigator.extensions.ExtensionSequenceNumberComparator; |
| import org.eclipse.ui.internal.navigator.extensions.NavigatorContentDescriptor; |
| import org.eclipse.ui.internal.navigator.extensions.NavigatorContentExtension; |
| import org.eclipse.ui.navigator.INavigatorContentDescriptor; |
| import org.eclipse.ui.navigator.INavigatorSaveablesService; |
| import org.eclipse.ui.navigator.SaveablesProvider; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.BundleEvent; |
| |
| /** |
| * Implementation of INavigatorSaveablesService. |
| * <p> |
| * Implementation note: all externally callable methods are synchronized. The |
| * private helper methods are not synchronized since they can only be called |
| * from methods that already hold the lock. |
| * </p> |
| * @since 3.2 |
| * |
| */ |
| public class NavigatorSaveablesService implements INavigatorSaveablesService, VisibilityListener { |
| |
| private NavigatorContentService contentService; |
| |
| private static List<NavigatorSaveablesService> instances = new ArrayList<>(); |
| |
| /** |
| * @param contentService |
| */ |
| public NavigatorSaveablesService(NavigatorContentService contentService) { |
| this.contentService = contentService; |
| } |
| |
| private static void addInstance(NavigatorSaveablesService saveablesService) { |
| synchronized (instances) { |
| instances.add(saveablesService); |
| } |
| } |
| |
| private static void removeInstance( |
| NavigatorSaveablesService saveablesService) { |
| synchronized (instances) { |
| instances.remove(saveablesService); |
| } |
| } |
| |
| /** |
| * @param event |
| */ |
| /* package */ static void bundleChanged(BundleEvent event) { |
| synchronized(instances) { |
| if (event.getType() == BundleEvent.STARTED) { |
| // System.out.println("bundle started: " + event.getBundle().getSymbolicName()); //$NON-NLS-1$ |
| for (NavigatorSaveablesService instance : instances) { |
| instance.handleBundleStarted(event.getBundle() |
| .getSymbolicName()); |
| } |
| } else if (event.getType() == BundleEvent.STOPPED) { |
| // System.out.println("bundle stopped: " + event.getBundle().getSymbolicName()); //$NON-NLS-1$ |
| for (NavigatorSaveablesService instance : instances) { |
| instance.handleBundleStopped(event.getBundle() |
| .getSymbolicName()); |
| } |
| } |
| } |
| } |
| |
| private class LifecycleListener implements ISaveablesLifecycleListener { |
| @Override |
| public void handleLifecycleEvent(SaveablesLifecycleEvent event) { |
| Saveable[] saveables = event.getSaveables(); |
| Saveable[] shownSaveables = null; |
| // synchronize in the same order as in the init method. |
| synchronized (instances) { |
| synchronized (NavigatorSaveablesService.this) { |
| if (isDisposed()) |
| return; |
| switch (event.getEventType()) { |
| case SaveablesLifecycleEvent.POST_OPEN: |
| recomputeSaveablesAndNotify(false, null); |
| break; |
| case SaveablesLifecycleEvent.POST_CLOSE: |
| recomputeSaveablesAndNotify(false, null); |
| break; |
| case SaveablesLifecycleEvent.DIRTY_CHANGED: |
| Set<Saveable> result = new HashSet<>(Arrays.asList(currentSaveables)); |
| result.retainAll(Arrays.asList(saveables)); |
| shownSaveables = result.toArray(new Saveable[result.size()]); |
| break; |
| } |
| } |
| } |
| |
| // Notify outside of synchronization |
| if (shownSaveables != null && shownSaveables.length > 0) { |
| outsideListener.handleLifecycleEvent(new SaveablesLifecycleEvent(saveablesSource, SaveablesLifecycleEvent.DIRTY_CHANGED, |
| shownSaveables, false)); |
| } |
| } |
| } |
| |
| private Saveable[] currentSaveables; |
| |
| private ISaveablesLifecycleListener outsideListener; |
| |
| private ISaveablesLifecycleListener saveablesLifecycleListener = new LifecycleListener(); |
| |
| private ISaveablesSource saveablesSource; |
| |
| private StructuredViewer viewer; |
| |
| private SaveablesProvider[] saveablesProviders; |
| |
| private DisposeListener disposeListener = e -> { |
| // synchronize in the same order as in the init method. |
| synchronized (instances) { |
| synchronized (NavigatorSaveablesService.this) { |
| if (saveablesProviders != null) { |
| for (SaveablesProvider saveablesProvider : saveablesProviders) { |
| saveablesProvider.dispose(); |
| } |
| } |
| removeInstance(NavigatorSaveablesService.this); |
| contentService = null; |
| currentSaveables = null; |
| outsideListener = null; |
| saveablesLifecycleListener = null; |
| saveablesSource = null; |
| viewer = null; |
| saveablesProviders = null; |
| disposeListener = null; |
| } |
| } |
| }; |
| |
| private Map<String, List> inactivePluginsWithSaveablesProviders; |
| |
| /** |
| * a TreeMap (NavigatorContentDescriptor->SaveablesProvider) which uses |
| * ExtensionPriorityComparator.INSTANCE as its Comparator |
| */ |
| private Map<NavigatorContentDescriptor, SaveablesProvider> saveablesProviderMap; |
| |
| /** |
| * Implementation note: This is not synchronized at the method level because it needs to |
| * synchronize on "instances" first, then on "this", to avoid potential deadlock. |
| * |
| * @param saveablesSource |
| * @param viewer |
| * @param outsideListener |
| * |
| */ |
| @Override |
| public void init(final ISaveablesSource saveablesSource, |
| final StructuredViewer viewer, |
| ISaveablesLifecycleListener outsideListener) { |
| // Synchronize on instances to make sure that we don't miss bundle started events. |
| synchronized (instances) { |
| // Synchronize on this because we are calling computeSaveables. |
| // Synchronization must remain in this order to avoid deadlock. |
| // This might not be necessary because at this time, no other |
| // concurrent calls should be possible, but it doesn't hurt either. |
| // For example, the initialization sequence might change in the |
| // future. |
| synchronized (this) { |
| this.saveablesSource = saveablesSource; |
| this.viewer = viewer; |
| this.outsideListener = outsideListener; |
| currentSaveables = computeSaveables(); |
| // add this instance after we are fully inialized. |
| addInstance(this); |
| } |
| } |
| viewer.getControl().addDisposeListener(disposeListener); |
| } |
| |
| private boolean isDisposed() { |
| return contentService == null; |
| } |
| |
| /** helper to compute the saveables for which elements are part of the tree. |
| * Must be called from a synchronized method. |
| * |
| * @return the saveables |
| */ |
| private Saveable[] computeSaveables() { |
| ITreeContentProvider contentProvider = (ITreeContentProvider) viewer |
| .getContentProvider(); |
| boolean isTreepathContentProvider = contentProvider instanceof ITreePathContentProvider; |
| Object viewerInput = viewer.getInput(); |
| List<Saveable> result = new ArrayList<>(); |
| Set<Object> roots = new HashSet<>(Arrays.asList(contentProvider |
| .getElements(viewerInput))); |
| SaveablesProvider[] saveablesProviders = getSaveablesProviders(); |
| for (SaveablesProvider saveablesProvider : saveablesProviders) { |
| Saveable[] saveables = saveablesProvider.getSaveables(); |
| for (Saveable saveable : saveables) { |
| Object[] elements = saveablesProvider.getElements(saveable); |
| // the saveable is added to the result if at least one of the |
| // elements representing the saveable appears in the tree, i.e. |
| // if its parent chain leads to a root node. |
| boolean foundRoot = false; |
| for (int k = 0; !foundRoot && k < elements.length; k++) { |
| Object element = elements[k]; |
| if (roots.contains(element)) { |
| result.add(saveable); |
| foundRoot = true; |
| } else if (isTreepathContentProvider) { |
| ITreePathContentProvider treePathContentProvider = (ITreePathContentProvider) contentProvider; |
| TreePath[] parentPaths = treePathContentProvider.getParents(element); |
| for (int l = 0; !foundRoot && l < parentPaths.length; l++) { |
| TreePath parentPath = parentPaths[l]; |
| for (int m = 0; !foundRoot && m < parentPath.getSegmentCount(); m++) { |
| if (roots.contains(parentPath.getSegment(m))) { |
| result.add(saveable); |
| foundRoot = true; |
| } |
| } |
| } |
| } else { |
| while (!foundRoot && element != null) { |
| if (roots.contains(element)) { |
| // found a parent chain leading to a root. The |
| // saveable is part of the tree. |
| result.add(saveable); |
| foundRoot = true; |
| } else { |
| element = contentProvider.getParent(element); |
| } |
| } |
| } |
| } |
| } |
| } |
| return result.toArray(new Saveable[result.size()]); |
| } |
| |
| @Override |
| public synchronized Saveable[] getActiveSaveables() { |
| if(!isDisposed()){ |
| ITreeContentProvider contentProvider = (ITreeContentProvider) viewer |
| .getContentProvider(); |
| IStructuredSelection selection = viewer.getStructuredSelection(); |
| if (selection instanceof ITreeSelection) { |
| return getActiveSaveablesFromTreeSelection((ITreeSelection) selection); |
| } else if (contentProvider instanceof ITreePathContentProvider) { |
| return getActiveSaveablesFromTreePathProvider(selection, (ITreePathContentProvider) contentProvider); |
| } else { |
| return getActiveSaveablesFromTreeProvider(selection, contentProvider); |
| } |
| } |
| return new Saveable[0]; |
| } |
| |
| /** |
| * @param selection |
| * @return the active saveables |
| */ |
| private Saveable[] getActiveSaveablesFromTreeSelection( |
| ITreeSelection selection) { |
| Set<Saveable> result = new HashSet<>(); |
| TreePath[] paths = selection.getPaths(); |
| for (TreePath path : paths) { |
| Saveable saveable = findSaveable(path); |
| if (saveable != null) { |
| result.add(saveable); |
| } |
| } |
| return result.toArray(new Saveable[result.size()]); |
| } |
| |
| /** |
| * @param selection |
| * @param provider |
| * @return the active saveables |
| */ |
| private Saveable[] getActiveSaveablesFromTreePathProvider( |
| IStructuredSelection selection, ITreePathContentProvider provider) { |
| Set<Saveable> result = new HashSet<>(); |
| for (Object element : selection) { |
| Saveable saveable = getSaveable(element); |
| if (saveable != null) { |
| result.add(saveable); |
| } else { |
| TreePath[] paths = provider.getParents(element); |
| saveable = findSaveable(paths); |
| if (saveable != null) { |
| result.add(saveable); |
| } |
| } |
| } |
| return result.toArray(new Saveable[result.size()]); |
| } |
| |
| /** |
| * @param selection |
| * @param contentProvider |
| * @return the active saveables |
| */ |
| private Saveable[] getActiveSaveablesFromTreeProvider( |
| IStructuredSelection selection, ITreeContentProvider contentProvider) { |
| Set<Saveable> result = new HashSet<>(); |
| for (Object element : selection) { |
| Saveable saveable = findSaveable(element, contentProvider); |
| if (saveable != null) { |
| result.add(saveable); |
| } |
| } |
| return result.toArray(new Saveable[result.size()]); |
| } |
| |
| /** |
| * @param element |
| * @param contentProvider |
| * @return the saveable, or null |
| */ |
| private Saveable findSaveable(Object element, |
| ITreeContentProvider contentProvider) { |
| while (element != null) { |
| Saveable saveable = getSaveable(element); |
| if (saveable != null) { |
| return saveable; |
| } |
| element = contentProvider.getParent(element); |
| } |
| return null; |
| } |
| |
| /** |
| * @param paths |
| * @return the saveable, or null |
| */ |
| private Saveable findSaveable(TreePath[] paths) { |
| for (TreePath path : paths) { |
| Saveable saveable = findSaveable(path); |
| if (saveable != null) { |
| return saveable; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param path |
| * @return a saveable, or null |
| */ |
| private Saveable findSaveable(TreePath path) { |
| int count = path.getSegmentCount(); |
| for (int j = count - 1; j >= 0; j--) { |
| Object parent = path.getSegment(j); |
| Saveable saveable = getSaveable(parent); |
| if (saveable != null) { |
| return saveable; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param element |
| * @return the saveable associated with the given element |
| */ |
| private Saveable getSaveable(Object element) { |
| if (saveablesProviderMap==null) { |
| // has the side effect of recomputing saveablesProviderMap: |
| getSaveablesProviders(); |
| } |
| for (Entry<NavigatorContentDescriptor, SaveablesProvider> entry : saveablesProviderMap.entrySet()) { |
| NavigatorContentDescriptor descriptor = entry.getKey(); |
| if(descriptor.isTriggerPoint(element) || descriptor.isPossibleChild(element)) { |
| SaveablesProvider provider = entry.getValue(); |
| Saveable saveable = provider.getSaveable(element); |
| if(saveable != null) { |
| return saveable; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @return the saveables |
| */ |
| @Override |
| public synchronized Saveable[] getSaveables() { |
| return currentSaveables; |
| } |
| |
| /** |
| * @return all SaveablesProvider objects |
| */ |
| private SaveablesProvider[] getSaveablesProviders() { |
| // TODO optimize this |
| if (saveablesProviders == null) { |
| if (isDisposed()) |
| return null; |
| inactivePluginsWithSaveablesProviders = new HashMap<>(); |
| saveablesProviderMap = new TreeMap<>(ExtensionSequenceNumberComparator.INSTANCE); |
| INavigatorContentDescriptor[] descriptors = contentService |
| .getActiveDescriptorsWithSaveables(); |
| List<SaveablesProvider> result = new ArrayList<>(); |
| for (INavigatorContentDescriptor iDescriptor : descriptors) { |
| NavigatorContentDescriptor descriptor = (NavigatorContentDescriptor) iDescriptor; |
| String pluginId = descriptor |
| .getContribution().getPluginId(); |
| if (Platform.getBundle(pluginId).getState() != Bundle.ACTIVE) { |
| List<NavigatorContentDescriptor> inactiveDescriptors = inactivePluginsWithSaveablesProviders |
| .get(pluginId); |
| if (inactiveDescriptors == null) { |
| inactiveDescriptors = new ArrayList<>(); |
| inactivePluginsWithSaveablesProviders.put(pluginId, |
| inactiveDescriptors); |
| } |
| inactiveDescriptors.add(descriptor); |
| } else { |
| SaveablesProvider saveablesProvider = createSaveablesProvider(descriptor); |
| if (saveablesProvider != null) { |
| saveablesProvider.init(saveablesLifecycleListener); |
| result.add(saveablesProvider); |
| saveablesProviderMap.put(descriptor, saveablesProvider); |
| } |
| } |
| } |
| saveablesProviders = result |
| .toArray(new SaveablesProvider[result.size()]); |
| } |
| return saveablesProviders; |
| } |
| |
| /** |
| * @param descriptor |
| * @return the SaveablesProvider, or null |
| */ |
| private SaveablesProvider createSaveablesProvider(NavigatorContentDescriptor descriptor) { |
| NavigatorContentExtension extension = contentService |
| .getExtension(descriptor, true); |
| // Use getContentProvider to get the client objects, this is important |
| // for the adaptation below. See bug 306545 |
| ITreeContentProvider contentProvider = extension |
| .getContentProvider(); |
| |
| return Adapters.adapt(contentProvider, SaveablesProvider.class); |
| } |
| |
| private void recomputeSaveablesAndNotify(boolean recomputeProviders, |
| String startedBundleIdOrNull) { |
| if (recomputeProviders && startedBundleIdOrNull == null |
| && saveablesProviders != null) { |
| // a bundle was stopped, dispose of all saveablesProviders and |
| // recompute |
| // TODO optimize this |
| for (SaveablesProvider saveablesProvider : saveablesProviders) { |
| saveablesProvider.dispose(); |
| } |
| saveablesProviders = null; |
| } else if (startedBundleIdOrNull != null){ |
| if(inactivePluginsWithSaveablesProviders.containsKey(startedBundleIdOrNull)) { |
| updateSaveablesProviders(startedBundleIdOrNull); |
| } |
| } |
| Set<Saveable> oldSaveables = new HashSet<>(Arrays.asList(currentSaveables)); |
| currentSaveables = computeSaveables(); |
| Set<Saveable> newSaveables = new HashSet<>(Arrays.asList(currentSaveables)); |
| final Set<Saveable> removedSaveables = new HashSet<>(oldSaveables); |
| removedSaveables.removeAll(newSaveables); |
| final Set<Saveable> addedSaveables = new HashSet<>(newSaveables); |
| addedSaveables.removeAll(oldSaveables); |
| if (addedSaveables.size() > 0) { |
| Display.getDefault().asyncExec(() -> { |
| if (isDisposed()) { |
| return; |
| } |
| outsideListener.handleLifecycleEvent( |
| new SaveablesLifecycleEvent(saveablesSource, SaveablesLifecycleEvent.POST_OPEN, |
| addedSaveables.toArray(new Saveable[addedSaveables.size()]), false)); |
| }); |
| } |
| // TODO this will make the closing of saveables non-cancelable. |
| // Ideally, we should react to PRE_CLOSE events and fire |
| // an appropriate PRE_CLOSE |
| if (removedSaveables.size() > 0) { |
| Display.getDefault().asyncExec(() -> { |
| if (isDisposed()) { |
| return; |
| } |
| outsideListener.handleLifecycleEvent( |
| new SaveablesLifecycleEvent(saveablesSource, SaveablesLifecycleEvent.PRE_CLOSE, |
| removedSaveables.toArray(new Saveable[removedSaveables.size()]), true)); |
| outsideListener.handleLifecycleEvent( |
| new SaveablesLifecycleEvent(saveablesSource, SaveablesLifecycleEvent.POST_CLOSE, |
| removedSaveables.toArray(new Saveable[removedSaveables.size()]), false)); |
| }); |
| } |
| } |
| |
| /** |
| * @param startedBundleId |
| */ |
| private void updateSaveablesProviders(String startedBundleId) { |
| List<SaveablesProvider> result = new ArrayList<>(Arrays.asList(saveablesProviders)); |
| List descriptors = inactivePluginsWithSaveablesProviders |
| .get(startedBundleId); |
| for (Iterator it = descriptors.iterator(); it.hasNext();) { |
| NavigatorContentDescriptor descriptor = (NavigatorContentDescriptor) it |
| .next(); |
| SaveablesProvider saveablesProvider = createSaveablesProvider(descriptor); |
| if (saveablesProvider != null) { |
| saveablesProvider.init(saveablesLifecycleListener); |
| result.add(saveablesProvider); |
| saveablesProviderMap.put(descriptor, saveablesProvider); |
| } |
| } |
| saveablesProviders = result |
| .toArray(new SaveablesProvider[result.size()]); |
| } |
| |
| /** |
| * @param symbolicName |
| */ |
| private synchronized void handleBundleStarted(String symbolicName) { |
| if (!isDisposed()) { |
| if (inactivePluginsWithSaveablesProviders.containsKey(symbolicName)) { |
| recomputeSaveablesAndNotify(true, symbolicName); |
| } |
| } |
| } |
| |
| /** |
| * @param symbolicName |
| */ |
| private synchronized void handleBundleStopped(String symbolicName) { |
| if (!isDisposed()) { |
| recomputeSaveablesAndNotify(true, null); |
| } |
| } |
| |
| @Override |
| public synchronized void onVisibilityOrActivationChange() { |
| if (!isDisposed()) { |
| recomputeSaveablesAndNotify(true, null); |
| } |
| } |
| |
| @Override |
| public boolean hasSaveablesProvider() { |
| return !isDisposed() && getSaveablesProviders() != null && getSaveablesProviders().length > 0; |
| } |
| } |