/******************************************************************************* * Copyright (c) 2012 Pivotal Software, Inc. * 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: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.springsource.ide.eclipse.dashboard.internal.ui.discovery; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.operation.IRunnableContext; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.operation.ModalContext; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.jface.window.IShellProvider; import org.eclipse.jface.wizard.ProgressMonitorPart; import org.eclipse.mylyn.commons.core.DelegatingProgressMonitor; import org.eclipse.mylyn.commons.ui.CommonUiUtil; import org.eclipse.mylyn.internal.discovery.core.model.ConnectorDescriptor; import org.eclipse.mylyn.internal.discovery.core.model.ConnectorDiscovery; import org.eclipse.mylyn.internal.discovery.core.model.DiscoveryConnector; import org.eclipse.mylyn.internal.discovery.ui.AbstractInstallJob; import org.eclipse.mylyn.internal.discovery.ui.DiscoveryUi; import org.eclipse.mylyn.internal.discovery.ui.InstalledItem; import org.eclipse.mylyn.internal.discovery.ui.UninstallRequest; import org.eclipse.mylyn.internal.discovery.ui.wizards.DiscoveryViewer; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.ui.dialogs.PreferencesUtil; import org.eclipse.ui.forms.IManagedForm; import org.eclipse.ui.forms.editor.FormEditor; import org.eclipse.ui.forms.events.HyperlinkAdapter; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.Hyperlink; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.statushandlers.StatusManager; import org.osgi.framework.Version; import org.springframework.util.StringUtils; import org.springsource.ide.eclipse.commons.core.ResourceProvider; import org.springsource.ide.eclipse.commons.internal.configurator.Activator; import org.springsource.ide.eclipse.commons.internal.configurator.IConfigurator; import org.springsource.ide.eclipse.dashboard.internal.ui.IdeUiPlugin; import org.springsource.ide.eclipse.dashboard.internal.ui.util.IdeUiUtils; import org.springsource.ide.eclipse.dashboard.ui.AbstractDashboardPage; import org.springsource.ide.eclipse.dashboard.ui.IEnabledDashboardPart; /** * @author Steffen Pingel * @author Christian Dupuis * @author Terry Denney */ public class DashboardExtensionsPage extends AbstractDashboardPage implements IRunnableContext, IEnabledDashboardPart { static final String MAGIC_STOP_THE_MADNESS_NO_UNINSTALL_SYSPROP = "no.auto.m2e.uninstall"; static final boolean DONT_DO_UNINSTALL = Boolean.parseBoolean(System.getProperty( MAGIC_STOP_THE_MADNESS_NO_UNINSTALL_SYSPROP, Boolean.FALSE.toString())); static final String ID_PREFERENCE_PAGE = "com.springsource.sts.ide.ui.preferencePage.AutoConfiguration"; public static final String RESOURCE_DISCOVERY_DIRECTORY = "discovery.directory"; public static final Map<String, List<String>> FEATURE_MAPPING; public static final Set<String> SVN_FEATURES = Collections.unmodifiableSet(new HashSet<String>(Arrays .asList(new String[] { "org.eclipse.team.svn", "org.tigris.subversion.subclipse", "com.collabnet.desktop.feature", }))); public static final String OLD_M2E_EXTENSION_ID = "org.maven.ide.eclipse.feature"; public static final String NEW_M2E_EXTENSION_ID = "org.eclipse.m2e.feature"; public static final Set<String> M2E_EXTENSION_IDS = Collections.unmodifiableSet(new HashSet<String>(Arrays .asList(new String[] { OLD_M2E_EXTENSION_ID, NEW_M2E_EXTENSION_ID }))); public static final Set<String> NEW_M2E_FEATURES = Collections.unmodifiableSet(new HashSet<String>(Arrays .asList(new String[] { "org.eclipse.m2e.feature.feature.group", "org.eclipse.m2e.logback.feature.feature.group", "org.sonatype.m2e.mavenarchiver.feature.feature.group", "org.sonatype.m2e.buildhelper.feature.feature.group", "org.maven.ide.eclipse.wtp.feature.feature.group", "org.maven.ide.eclipse.ajdt.feature.feature.group" }))); public static final Set<String> OLD_M2E_FEATURES = Collections.unmodifiableSet(new HashSet<String>(Arrays .asList(new String[] { "org.maven.ide.eclipse.feature.feature.group" }))); private ProgressMonitorPart progressMonitorPart; private long activeRunningOperations = 0; private Button installButton; private DashboardDiscoveryViewer discoveryViewer; private Button cancelButton; private final DelegatingProgressMonitor monitor = new DelegatingProgressMonitor(); private Button findUpdatesButton; public static final String ID = "extensions"; static { // move that into an extension install/properties file FEATURE_MAPPING = new HashMap<String, List<String>>(); FEATURE_MAPPING.put("com.google.gwt.eclipse.core", Arrays.asList("com.google.gdt.eclipse.suite.e35.feature", "com.google.appengine.eclipse.sdkbundle.e35.feature.1.3.", "com.google.gwt.eclipse.sdkbundle.e35.feature.2.1.0", "com.google.gdt.eclipse.suite.e36.feature", "com.google.appengine.eclipse.sdkbundle.e36.feature.1.3.5", "com.google.gwt.eclipse.sdkbundle.e36.feature.2.1.0")); FEATURE_MAPPING.put("org.datanucleus.ide.eclipse", Collections.singletonList("org.datanucleus.ide.eclipse.feature")); } public DashboardExtensionsPage(FormEditor editor) { super(editor, ID, "Extensions"); } public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException { // The operation can only be canceled if it is executed in a separate // thread. // Otherwise the UI is blocked anyway. if (activeRunningOperations == 0) { aboutToStart(fork && cancelable); } activeRunningOperations++; try { ModalContext.run(runnable, fork, monitor, getManagedForm().getForm().getShell().getDisplay()); } finally { activeRunningOperations--; // Stop if this is the last one if (activeRunningOperations <= 0) { stopped(); } } } public boolean shouldAdd() { String url = ResourceProvider.getUrl(RESOURCE_DISCOVERY_DIRECTORY); return StringUtils.hasText(url); } /** * About to start a long running operation triggered through the wizard. * Shows the progress monitor and disables the wizard's buttons and * controls. * * @param enableCancelButton <code>true</code> if the Cancel button should * be enabled, and <code>false</code> if it should be disabled * @return the saved UI state */ private void aboutToStart(boolean enableCancelButton) { cancelButton.setVisible(true); cancelButton.setEnabled(true); installButton.setEnabled(false); findUpdatesButton.setEnabled(false); CommonUiUtil.setEnabled((Composite) discoveryViewer.getControl(), false); } private void adaptRecursively(Control control, FormToolkit toolkit) { toolkit.adapt(control, false, false); if (control instanceof Composite) { for (Control child : ((Composite) control).getChildren()) { adaptRecursively(child, toolkit); } } } private void initialize(final DashboardDiscoveryViewer viewer) { Dictionary<Object, Object> environment = viewer.getEnvironment(); // add the installed version to the environment so that we can // have connectors that are filtered based on the version Version version = IdeUiUtils.getVersion(); environment.put("com.springsource.sts.version", version.toString()); //$NON-NLS-1$ environment.put("com.springsource.sts.version.major", version.getMajor()); environment.put("com.springsource.sts.version.minor", version.getMinor()); environment.put("com.springsource.sts.version.micro", version.getMicro()); environment.put("com.springsource.sts.nightly", version.getQualifier().contains("-CI-")); version = IdeUiUtils.getPlatformVersion(); environment.put("platform.version", version.toString()); //$NON-NLS-1$ environment.put("platform.major", version.getMajor()); environment.put("platform.minor", version.getMinor()); environment.put("platform.micro", version.getMicro()); environment.put("platform", version.getMajor() + "." + version.getMinor()); viewer.setEnvironment(environment); viewer.setShowInstalledFilterEnabled(true); viewer.addFilter(new ViewerFilter() { private Boolean svnInstalled; @Override public boolean select(Viewer viewer, Object parentElement, Object element) { DiscoveryConnector connector = (DiscoveryConnector) element; // filter out Collabnet on non-Windows platforms if (connector.getId().startsWith("com.collabnet") && !Platform.getOS().equals(Platform.OS_WIN32)) { return false; } // don't show SVN team providers if one is already // installed unless it is the installed connector if (SVN_FEATURES.contains(connector.getId()) && !isInstalled(connector) && isSvnInstalled()) { return false; } // filter out Atlassian JIRA connector if (connector.getId().startsWith("com.atlassian")) { return false; } return true; } private boolean isInstalled(DiscoveryConnector connector) { Set<String> installedFeatures = viewer.getInstalledFeatures(); return installedFeatures != null && installedFeatures.contains(connector.getId() + ".feature.group"); } /** * Returns true, if an SVN team provider is installed. */ private boolean isSvnInstalled() { if (svnInstalled == null) { svnInstalled = Boolean.FALSE; Set<String> installedFeatures = viewer.getInstalledFeatures(); if (installedFeatures != null) { for (String svn : SVN_FEATURES) { if (installedFeatures.contains(svn + ".feature.group")) { svnInstalled = Boolean.TRUE; break; } } } } return svnInstalled.booleanValue(); } }); } private void stopped() { if (getManagedForm().getForm() == null || getManagedForm().getForm().isDisposed()) { return; } if (!monitor.isCanceled()) { discoveryViewer.setSelection(StructuredSelection.EMPTY); } // refresh list to update selection state discoveryViewer.createBodyContents(); progressMonitorPart.done(); cancelButton.setVisible(false); cancelButton.setEnabled(false); installButton.setEnabled(discoveryViewer.isComplete()); findUpdatesButton.setEnabled(true); CommonUiUtil.setEnabled((Composite) discoveryViewer.getControl(), true); } public boolean isRelatedToM2e(final Set<String> featuresToUninstall, String featureId) { if (DONT_DO_UNINSTALL) { return false; } return featuresToUninstall.contains(featureId) || featureId.contains("m2e") || featureId.contains("maven"); } @Override protected void createFormContent(final IManagedForm managedForm) { Composite body = managedForm.getForm().getBody(); body.setLayout(new GridLayout(5, false)); discoveryViewer = new DashboardDiscoveryViewer(getSite(), this); initialize(discoveryViewer); discoveryViewer.setDirectoryUrl(ResourceProvider.getUrl(RESOURCE_DISCOVERY_DIRECTORY).replace("%VERSION%", IdeUiUtils.getShortVersion())); discoveryViewer.setShowConnectorDescriptorKindFilter(false); discoveryViewer.createControl(body); FormToolkit toolkit = managedForm.getToolkit(); adaptRecursively(discoveryViewer.getControl(), toolkit); GridDataFactory.fillDefaults().span(5, 1).grab(true, true).applyTo(discoveryViewer.getControl()); findUpdatesButton = toolkit.createButton(body, "&Find Updates", SWT.NONE); findUpdatesButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { IHandlerService handlerService = (IHandlerService) getSite().getService(IHandlerService.class); try { handlerService.executeCommand("org.eclipse.equinox.p2.ui.sdk.update", new Event()); } catch (Exception e) { StatusManager.getManager().handle( new Status(IStatus.ERROR, IdeUiPlugin.PLUGIN_ID, "Find updates failed with an unexpected error.", e), StatusManager.SHOW | StatusManager.LOG); } } }); Hyperlink configureLink = toolkit.createHyperlink(body, "Configure Extensions...", SWT.NONE); configureLink.addHyperlinkListener(new HyperlinkAdapter() { @Override public void linkActivated(HyperlinkEvent event) { PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(getSite().getShell(), ID_PREFERENCE_PAGE, new String[] { ID_PREFERENCE_PAGE }, null); dialog.open(); } }); progressMonitorPart = new ProgressMonitorPart(body, null); monitor.attach(progressMonitorPart); progressMonitorPart.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { monitor.setCanceled(true); monitor.detach(progressMonitorPart); } }); adaptRecursively(progressMonitorPart, toolkit); GridDataFactory.fillDefaults().grab(true, false).applyTo(progressMonitorPart); // Button refreshButton = toolkit.createButton(body, "Refresh", // SWT.NONE); // refreshButton.addSelectionListener(new SelectionAdapter() { // @Override // public void widgetSelected(SelectionEvent e) { // pane.updateDiscovery(); // } // }); cancelButton = toolkit.createButton(body, "&Cancel", SWT.NONE); cancelButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { cancelButton.setEnabled(false); progressMonitorPart.setCanceled(true); } }); installButton = toolkit.createButton(body, "&Install", SWT.NONE); installButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { // check for conflicts List<ConnectorDescriptor> selection = discoveryViewer.getInstallableConnectors(); IStatus conflictStatus = new MultiStatus(IdeUiPlugin.PLUGIN_ID, -1, new IStatus[] { checkForConflicts(SVN_FEATURES, " Please select only one SVN team provider.", selection), checkForConflicts(M2E_EXTENSION_IDS, " Please select only one m2e version to install.", selection) }, "Could not perform install due to conflicts.", null); if (!conflictStatus.isOK()) { StatusManager.getManager().handle(conflictStatus, StatusManager.SHOW | StatusManager.BLOCK); return; } // now, if m2e is going to be installed, ensure that all other // versions of m2e are uninstalled first. Set<String> featuresToUninstall = chooseUnwantedFeatures(selection); if (!featuresToUninstall.isEmpty()) { IStatus uninstallResult = uninstallFeatures(featuresToUninstall); if (!uninstallResult.isOK()) { if (uninstallResult.getSeverity() != IStatus.CANCEL) { StatusManager.getManager().handle(uninstallResult, StatusManager.SHOW | StatusManager.LOG | StatusManager.BLOCK); } return; } } DiscoveryUi.install(discoveryViewer.getInstallableConnectors(), DashboardExtensionsPage.this); } private IStatus uninstallFeatures(final Set<String> featuresToUninstall) { String allInstalled = findFeaturesToUninstall(featuresToUninstall, discoveryViewer.getInstalledFeatures()); if (allInstalled.length() == 0) { return Status.OK_STATUS; } boolean res = MessageDialog.openQuestion(getPartControl().getShell(), "Perform uninstall?", "In order to switch versions of m2eclipse, the following features will be uninstalled:\n" + allInstalled + "Do you want to continue?"); if (!res) { return Status.CANCEL_STATUS; } // uninstall previous Maven tooling AbstractInstallJob job = DiscoveryUi.createInstallJob(); try { return job.uninstall(new UninstallRequest() { /** * Uninstall all features that are somehow related to * m2eclipse */ @Override public boolean select(InstalledItem item) { String featureId = item.getId(); return isRelatedToM2e(featuresToUninstall, featureId); } }, new NullProgressMonitor()); } catch (Exception e) { return new Status(IStatus.ERROR, IdeUiPlugin.PLUGIN_ID, NLS.bind( "Could not uninstall features:\n{0},\n try uninstalling manually.", featuresToUninstall), e); } } private String findFeaturesToUninstall(Set<String> featuresToUninstall, Set<String> installedFeatures) { StringBuilder sb = new StringBuilder(); for (String featureId : installedFeatures) { if (isRelatedToM2e(featuresToUninstall, featureId)) { if (featureId.endsWith(".feature.group")) { featureId = featureId.substring(0, featureId.length() - ".feature.group".length()); } sb.append(" " + featureId + "\n"); } } return sb.toString(); } private Set<String> chooseUnwantedFeatures(List<ConnectorDescriptor> selection) { boolean uninstallOld = false; boolean uninstallNew = false; for (ConnectorDescriptor feature : selection) { // if new m2e to be installed, then must uninstall the old // first and vice versa if (feature.getId().equals(NEW_M2E_EXTENSION_ID)) { uninstallOld = true; } else if (feature.getId().equals(OLD_M2E_EXTENSION_ID)) { uninstallNew = true; } } Set<String> maybeUninstall; if (uninstallOld) { maybeUninstall = OLD_M2E_FEATURES; } else if (uninstallNew) { maybeUninstall = NEW_M2E_FEATURES; } else { maybeUninstall = Collections.emptySet(); } Set<String> installedFeatures = DashboardExtensionsPage.this.discoveryViewer.getInstalledFeatures(); Set<String> definitelyUninstall = new HashSet<String>(); for (String feature : maybeUninstall) { if (installedFeatures.contains(feature)) { definitelyUninstall.add(feature); } } if (definitelyUninstall.size() > 0) { IdeUiPlugin.log(new Status(IStatus.INFO, IdeUiPlugin.PLUGIN_ID, "To make way for a new version of m2eclipse, we will uninstall these features: " + definitelyUninstall)); } return definitelyUninstall; } /** * This method checks for conflicts with requested installs. If a * conflict is found, this method will pop up a dialog explaining * the conflict and it will return true. * @param featuresToCheck set of features of which only one can be * installed at once. * @param message message to add if there is an error * @param selection * * @return true iff there is a conflict. */ public IStatus checkForConflicts(Set<String> featuresToCheck, String prependedMessage, List<ConnectorDescriptor> selection) { StringBuilder message = new StringBuilder(); List<ConnectorDescriptor> conflicting = new ArrayList<ConnectorDescriptor>(); for (ConnectorDescriptor descriptor : selection) { if (featuresToCheck.contains(descriptor.getId())) { conflicting.add(descriptor); if (message.length() > 0) { message.append(", "); } message.append(descriptor.getName()); } } if (conflicting.size() > 1) { return new Status(IStatus.WARNING, IdeUiPlugin.PLUGIN_ID, NLS.bind( "The following extensions can not be installed at the same time: {0}.", message.toString())); } else { return Status.OK_STATUS; } } }); Display.getCurrent().asyncExec(new Runnable() { public void run() { if (!managedForm.getForm().isDisposed()) { discoveryViewer.updateDiscovery(); } } }); discoveryViewer.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { installButton.setEnabled(discoveryViewer.isComplete()); } }); } private final class DashboardDiscoveryViewer extends DiscoveryViewer { private static final String READ_ONLY_MESSAGE = "Cannot install Groovy-Eclipse because STS installation directory is read-only. " + "To install Groovy-Eclipse. please make sure that the STS install location is writable by the current user. "; private static final String PROGRAM_FILES_MESSAGE = "Cannot install Groovy-Eclipse because STS is located in 'C:\\Program Files'. " + "To install Groovy-Eclipse, please change the location of STS and try again."; private static final String GROOVY_FEATURE_PREFIX = "org.codehaus.groovy"; private Set<String> installedFeatures; private DashboardDiscoveryViewer(IShellProvider shellProvider, IRunnableContext context) { super(shellProvider, context); } public Set<String> getInstalledFeatures() { return installedFeatures; } @Override protected Set<String> getInstalledFeatures(IProgressMonitor monitor) throws InterruptedException { this.installedFeatures = super.getInstalledFeatures(monitor); IConfigurator configurator = Activator.getConfigurator(); if (configurator != null) { installedFeatures.addAll(configurator.getInstalledBundles()); } for (Map.Entry<String, List<String>> entry : FEATURE_MAPPING.entrySet()) { if (Platform.getBundle(entry.getKey()) != null) { installedFeatures.addAll(entry.getValue()); } } return installedFeatures; } @Override protected void postDiscovery(ConnectorDiscovery connectorDiscovery) { super.postDiscovery(connectorDiscovery); for (DiscoveryConnector connector : connectorDiscovery.getConnectors()) { if (connector.getSiteUrl() != null && connector.getSiteUrl().endsWith("-disabled")) { connector.setAvailable(Boolean.FALSE); } // disable Groovy-Eclipse for read-only installations // and provide an explanation why if (connector.getId() != null && connector.getId().startsWith(GROOVY_FEATURE_PREFIX)) { File file = getInstallLocation(); boolean readOnly = isReadOnly(file); boolean inProgramFiles = isInProgramFiles(file); if (readOnly || inProgramFiles) { connector.setAvailable(Boolean.FALSE); connector.setName(connector.getName() + " (Cannot install)"); connector.setDescription(inProgramFiles ? PROGRAM_FILES_MESSAGE : READ_ONLY_MESSAGE + connector.getDescription()); } } } } private File getInstallLocation() { URL url = Platform.getInstallLocation().getURL(); if (url != null) { return new File(url.getFile()); } else { return null; } } private boolean isReadOnly(File installFolder) { if (installFolder == null) { return false; } File configurationFolder = new File("configuration"); // consider read-only if either the install location or the default // config folder is read-only. return (installFolder.exists() && !installFolder.canWrite()) || (configurationFolder.exists() && !configurationFolder.canWrite()); } private boolean isInProgramFiles(File installFolder) { if (installFolder == null) { return false; } String absolutePath = installFolder.getAbsolutePath(); return installFolder.exists() && (absolutePath.startsWith("C:\\Program Files") || absolutePath.startsWith("C:/Program Files")); } } }