/******************************************************************************* * Copyright (c) 2006, 2015 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.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.DisposeEvent; 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<NavigatorSaveablesService>(); /** * @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<Saveable>(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 = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent 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<Saveable>(); Set<Object> roots = new HashSet<Object>(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<Saveable>(); 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<Saveable>(); for (Iterator it = selection.iterator(); it.hasNext();) { Object element = it.next(); 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<Saveable>(); for (Iterator it = selection.iterator(); it.hasNext();) { Object element = it.next(); 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) { inactivePluginsWithSaveablesProviders = new HashMap<String, List>(); saveablesProviderMap = new TreeMap<NavigatorContentDescriptor, SaveablesProvider>(ExtensionSequenceNumberComparator.INSTANCE); INavigatorContentDescriptor[] descriptors = contentService .getActiveDescriptorsWithSaveables(); List<SaveablesProvider> result = new ArrayList<SaveablesProvider>(); 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<NavigatorContentDescriptor>(); 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<Saveable>(Arrays.asList(currentSaveables)); currentSaveables = computeSaveables(); Set<Saveable> newSaveables = new HashSet<Saveable>(Arrays.asList(currentSaveables)); final Set<Saveable> removedSaveables = new HashSet<Saveable>(oldSaveables); removedSaveables.removeAll(newSaveables); final Set<Saveable> addedSaveables = new HashSet<Saveable>(newSaveables); addedSaveables.removeAll(oldSaveables); if (addedSaveables.size() > 0) { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { 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(new Runnable() { @Override public void run() { 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<SaveablesProvider>(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); } } }