/*******************************************************************************
* Copyright (c) 2009, 2013 Tasktop Technologies 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:
* Tasktop Technologies - initial API and implementation
* JBoss (Pascal Rapicault) - Bug 406907 - Add p2 remediation page to MPC install flow
* Yatta Solutions - bug 432803: public API
*******************************************************************************/
package org.eclipse.epp.internal.mpc.ui.operations;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.epp.internal.mpc.ui.MarketplaceClientUi;
import org.eclipse.epp.internal.mpc.ui.wizards.SelectionModel.FeatureEntry;
import org.eclipse.epp.mpc.ui.Operation;
import org.eclipse.equinox.internal.p2.discovery.model.CatalogItem;
import org.eclipse.equinox.internal.p2.ui.discovery.util.WorkbenchUtil;
import org.eclipse.equinox.p2.engine.ProvisioningContext;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.operations.InstallOperation;
import org.eclipse.equinox.p2.operations.ProfileChangeOperation;
import org.eclipse.equinox.p2.operations.ProvisioningSession;
import org.eclipse.equinox.p2.operations.RemediationOperation;
import org.eclipse.equinox.p2.operations.RepositoryTracker;
import org.eclipse.equinox.p2.operations.UninstallOperation;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository;
import org.eclipse.equinox.p2.ui.ProvisioningUI;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;
/**
* A job that configures a p2 provisioning operation for installing/updating/removing one or more {@link CatalogItem
* connectors}. The bulk of the installation work is done by p2; this class just sets up the p2 repository meta-data and
* selects the appropriate features to install.
*
* @author David Green
* @author Steffen Pingel
*/
public class ProfileChangeOperationComputer extends AbstractProvisioningOperation {
public enum ResolutionStrategy {
ALL_REPOSITORIES, SELECTED_REPOSITORIES, FALLBACK_STRATEGY
}
public enum OperationType {
/**
* Install and update features
*/
INSTALL,
/**
* update only, use {@link #INSTALL} if features are to be installed and updated in a single operation
*/
UPDATE,
/**
* uninstall features
*/
UNINSTALL,
/**
* Change an installation - this might involve any of the above operations
*/
CHANGE;
public static OperationType map(org.eclipse.epp.mpc.ui.Operation operation) {
if (operation == null) {
return null;
}
switch (operation) {
case INSTALL:
return INSTALL;
case UNINSTALL:
return UNINSTALL;
case UPDATE:
return UPDATE;
case CHANGE:
return CHANGE;
case NONE:
return null;
default:
throw new IllegalArgumentException(NLS.bind(Messages.ProfileChangeOperationComputer_unknownOperation,
operation));
}
}
}
private final OperationType operationType;
private final List<FeatureEntry> featureEntries;
private ProfileChangeOperation operation;
private IInstallableUnit[] ius;
private final ResolutionStrategy resolutionStrategy;
private final URI dependenciesRepository;
private final boolean withRemediation;
private String errorMessage;
/**
* @param operationType
* the type of operation to perform
* @param items
* the items for which features are being installed
* @param featureEntries
* the features to install/update/uninstall, which must correspond to features provided by the given
* items
* @param dependenciesRepository
* an optional URI to a repository from which dependencies may be installed, may be null
*/
public ProfileChangeOperationComputer(OperationType operationType, Collection<CatalogItem> items, Collection<FeatureEntry> featureEntries,
URI dependenciesRepository, ResolutionStrategy resolutionStrategy, boolean withRemediation) {
super(items);
if (featureEntries == null || featureEntries.isEmpty()) {
throw new IllegalArgumentException();
}
if (operationType == null) {
throw new IllegalArgumentException();
}
if (resolutionStrategy == null) {
throw new IllegalArgumentException();
}
this.featureEntries = new ArrayList<FeatureEntry>(featureEntries);
this.operationType = operationType;
this.resolutionStrategy = resolutionStrategy;
this.dependenciesRepository = dependenciesRepository;
this.withRemediation = withRemediation;
}
public void run(IProgressMonitor progressMonitor) throws InvocationTargetException, InterruptedException {
try {
boolean hasInstall = hasInstall();
boolean hasUninstall = hasUninstall();
SubMonitor monitor = SubMonitor.convert(progressMonitor,
Messages.ProvisioningOperation_configuringProvisioningOperation, 1000 + (hasInstall ? 500 : 0)
+ (hasUninstall ? 100 : 0));
try {
IInstallableUnit[] uninstallIUs = null;
if (hasInstall) {
ius = computeInstallableUnits(monitor.newChild(500));
} else {
ius = new IInstallableUnit[0];
}
if (hasUninstall) {
uninstallIUs = computeUninstallUnits(monitor.newChild(100));
}
checkCancelled(monitor);
URI[] repositories = repositoryLocations == null ? new URI[0] : repositoryLocations.toArray(new URI[0]);
switch (operationType) {
case INSTALL:
operation = resolveInstall(monitor.newChild(500), ius, repositories);
break;
case UPDATE:
operation = resolveUpdate(monitor.newChild(500), computeInstalledIus(ius), repositories);
break;
case UNINSTALL:
operation = resolveUninstall(monitor.newChild(500), uninstallIUs, repositories);
break;
case CHANGE:
operation = resolveChange(monitor.newChild(500), ius, uninstallIUs, repositories);
break;
default:
throw new UnsupportedOperationException(operationType.name());
}
if (withRemediation && operation != null && operation.getResolutionResult().getSeverity() == IStatus.ERROR) {
RemediationOperation remediationOperation = new RemediationOperation(ProvisioningUI.getDefaultUI()
.getSession(), operation.getProfileChangeRequest());
remediationOperation.resolveModal(monitor.newChild(500));
if (remediationOperation.getResolutionResult() == Status.OK_STATUS) {
errorMessage = operation.getResolutionDetails();
operation = remediationOperation;
}
}
checkCancelled(monitor);
} finally {
monitor.done();
}
} catch (OperationCanceledException e) {
throw new InterruptedException();
} catch (Exception e) {
throw new InvocationTargetException(e);
}
}
private boolean hasInstall() {
switch (operationType) {
case INSTALL:
case UPDATE:
return true;
case UNINSTALL:
return false;
case CHANGE:
for (FeatureEntry entry : featureEntries) {
Operation operation = entry.computeChangeOperation();
if (operation == Operation.INSTALL || operation == Operation.UPDATE) {
return true;
}
}
//$fall-through$
default:
return false;
}
}
private boolean hasUninstall() {
switch (operationType) {
case INSTALL:
case UPDATE:
return false;
case UNINSTALL:
return true;
case CHANGE:
for (FeatureEntry entry : featureEntries) {
if (entry.computeChangeOperation() == Operation.UNINSTALL) {
return true;
}
}
//$fall-through$
default:
return false;
}
}
public String getErrorMessage() {
return errorMessage;
}
private IInstallableUnit[] computeInstalledIus(IInstallableUnit[] ius) {
List<IInstallableUnit> installedIus = new ArrayList<IInstallableUnit>(ius.length);
Map<String, IInstallableUnit> iUsById = MarketplaceClientUi.computeInstalledIUsById(new NullProgressMonitor());
for (IInstallableUnit iu : ius) {
IInstallableUnit installedIu = iUsById.get(iu.getId());
installedIus.add(installedIu);
}
return installedIus.toArray(new IInstallableUnit[installedIus.size()]);
}
public ProfileChangeOperation getOperation() {
return operation;
}
public IInstallableUnit[] getIus() {
return ius;
}
private interface ProfileChangeOperationFactory {
ProfileChangeOperation create(List<IInstallableUnit> ius) throws CoreException;
}
private ProfileChangeOperation resolveInstall(IProgressMonitor monitor, final IInstallableUnit[] ius,
URI[] repositories) throws CoreException {
return resolve(monitor, new ProfileChangeOperationFactory() {
public ProfileChangeOperation create(List<IInstallableUnit> ius) throws CoreException {
return provisioningUI.getInstallOperation(ius, null);
}
}, ius, repositories);
}
private ProfileChangeOperation resolveUninstall(IProgressMonitor monitor, final IInstallableUnit[] ius,
URI[] repositories) throws CoreException {
return resolve(monitor, new ProfileChangeOperationFactory() {
public ProfileChangeOperation create(List<IInstallableUnit> ius) throws CoreException {
return provisioningUI.getUninstallOperation(ius, null);
}
}, ius, repositories);
}
private ProfileChangeOperation resolveUpdate(IProgressMonitor monitor, final IInstallableUnit[] ius,
URI[] repositories) throws CoreException {
return resolve(monitor, new ProfileChangeOperationFactory() {
public ProfileChangeOperation create(List<IInstallableUnit> ius) throws CoreException {
return provisioningUI.getUpdateOperation(ius, null);
}
}, ius, repositories);
}
private ProfileChangeOperation resolveChange(IProgressMonitor monitor, IInstallableUnit[] ius,
final IInstallableUnit[] uninstallIUs, URI[] repositories) throws CoreException {
return resolve(monitor, new ProfileChangeOperationFactory() {
public ProfileChangeOperation create(List<IInstallableUnit> ius) throws CoreException {
InstallOperation installOperation = provisioningUI.getInstallOperation(ius, null);
UninstallOperation uninstallOperation = provisioningUI.getUninstallOperation(
Arrays.asList(uninstallIUs), null);
CompositeProfileChangeOperation operation = new CompositeProfileChangeOperation(
provisioningUI.getSession());
operation.setProfileId(provisioningUI.getProfileId());
ProvisioningContext provisioningContext = installOperation.getProvisioningContext();
operation.setProvisioningContext(provisioningContext);
uninstallOperation.setProvisioningContext(provisioningContext);
operation.add(uninstallOperation).add(installOperation);
return operation;
}
}, ius, repositories);
}
private ProfileChangeOperation resolve(IProgressMonitor monitor, ProfileChangeOperationFactory operationFactory,
IInstallableUnit[] ius, URI[] repositories) throws CoreException {
List<IInstallableUnit> installableUnits = Arrays.asList(ius);
List<ResolutionStrategy> strategies = new ArrayList<ProfileChangeOperationComputer.ResolutionStrategy>(2);
switch (resolutionStrategy) {
case FALLBACK_STRATEGY:
strategies.add(ResolutionStrategy.SELECTED_REPOSITORIES);
strategies.add(ResolutionStrategy.ALL_REPOSITORIES);
break;
default:
strategies.add(resolutionStrategy);
}
ProvisioningSession session = ProvisioningUI.getDefaultUI().getSession();
RepositoryTracker repositoryTracker = ProvisioningUI.getDefaultUI().getRepositoryTracker();
URI[] knownRepositories = repositoryTracker.getKnownRepositories(session);
ProfileChangeOperation operation = null;
final int workPerStrategy = 1000;
SubMonitor subMonitor = SubMonitor.convert(monitor, strategies.size() * workPerStrategy + workPerStrategy);
Set<URI> previousRepositoryLocations = null;
for (ResolutionStrategy strategy : strategies) {
Set<URI> repositoryLocations = new HashSet<URI>(Arrays.asList(repositories));
if (strategy == ResolutionStrategy.SELECTED_REPOSITORIES) {
repositoryLocations.addAll(Arrays.asList(repositories));
}
if (dependenciesRepository != null) {
repositoryLocations.add(dependenciesRepository);
}
if (strategy == ResolutionStrategy.ALL_REPOSITORIES && !repositoryLocations.isEmpty()) {
repositoryLocations.addAll(Arrays.asList(knownRepositories));
}
if (repositoryLocations.equals(previousRepositoryLocations)) {
continue;
}
operation = operationFactory.create(installableUnits);
if (!repositoryLocations.isEmpty()) {
URI[] locations = repositoryLocations.toArray(new URI[repositoryLocations.size()]);
operation.getProvisioningContext().setMetadataRepositories(locations);
operation.getProvisioningContext().setArtifactRepositories(locations);
}
resolveModal(subMonitor.newChild(workPerStrategy), operation);
if (operation.getResolutionResult() != null
&& operation.getResolutionResult().getSeverity() != IStatus.ERROR) {
break;
}
previousRepositoryLocations = repositoryLocations;
}
return operation;
}
public void resolveModal(IProgressMonitor monitor, ProfileChangeOperation operation) throws CoreException {
operation.resolveModal(new SubProgressMonitor(monitor, items.size()));
}
public IInstallableUnit[] computeInstallableUnits(IProgressMonitor monitor) throws CoreException {
try {
SubMonitor progress = SubMonitor.convert(monitor, 100);
// add repository urls and load meta data
List<IMetadataRepository> repositories = addRepositories(progress.newChild(50));
List<IInstallableUnit> installableUnits = queryInstallableUnits(progress.newChild(50), repositories);
checkForUnavailable(installableUnits);
pruneNonInstall(installableUnits);
// bug 306446 we never want to downgrade the installed version
pruneOlderVersions(installableUnits);
return installableUnits.toArray(new IInstallableUnit[installableUnits.size()]);
} catch (URISyntaxException e) {
// should never happen, since we already validated URLs.
throw new CoreException(new Status(IStatus.ERROR, MarketplaceClientUi.BUNDLE_ID,
Messages.ProvisioningOperation_unexpectedErrorUrl, e));
} finally {
monitor.done();
}
}
public IInstallableUnit[] computeUninstallUnits(IProgressMonitor monitor) throws CoreException {
try {
// calculate installed ius
Map<String, IInstallableUnit> installedIUs = MarketplaceClientUi.computeInstalledIUsById(monitor);
List<IInstallableUnit> installableUnits = new ArrayList<IInstallableUnit>(installedIUs.values());
pruneNonUninstall(installableUnits);
return installableUnits.toArray(new IInstallableUnit[installableUnits.size()]);
} finally {
monitor.done();
}
}
/**
* Remove ius from the given list where the current profile already contains a newer version of that iu.
*
* @param installableUnits
* @throws CoreException
*/
private void pruneOlderVersions(List<IInstallableUnit> installableUnits) throws CoreException {
if (!installableUnits.isEmpty()) {
Map<String, IInstallableUnit> iUsById = MarketplaceClientUi.computeInstalledIUsById(new NullProgressMonitor());
Iterator<IInstallableUnit> it = installableUnits.iterator();
while (it.hasNext()) {
IInstallableUnit iu = it.next();
IInstallableUnit installedIu = iUsById.get(iu.getId());
if (installedIu != null) {
Version installedVersion = installedIu.getVersion();
Version installableVersion = iu.getVersion();
if (installedVersion.compareTo(installableVersion) >= 0) {
it.remove();
}
}
}
if (installableUnits.isEmpty()) {
throw new CoreException(new Status(IStatus.INFO, MarketplaceClientUi.BUNDLE_ID,
Messages.ProvisioningOperation_nothingToUpdate));
}
}
}
private void pruneNonInstall(List<IInstallableUnit> installableUnits) {
Set<String> installableFeatureIds = new HashSet<String>();
for (FeatureEntry featureEntry : featureEntries) {
Operation operation = featureEntry.computeChangeOperation();
if (operation == Operation.INSTALL || operation == Operation.UPDATE) {
installableFeatureIds.add(featureEntry.getFeatureDescriptor().getId());
}
}
Iterator<IInstallableUnit> it = installableUnits.iterator();
while (it.hasNext()) {
IInstallableUnit iu = it.next();
if (!installableFeatureIds.contains(iu.getId())) {
it.remove();
}
}
}
private void pruneNonUninstall(List<IInstallableUnit> installableUnits) {
Set<String> installableFeatureIds = new HashSet<String>();
for (FeatureEntry featureEntry : featureEntries) {
if (featureEntry.computeChangeOperation() == Operation.UNINSTALL) {
installableFeatureIds.add(featureEntry.getFeatureDescriptor().getId());
}
}
Iterator<IInstallableUnit> it = installableUnits.iterator();
while (it.hasNext()) {
IInstallableUnit iu = it.next();
if (!installableFeatureIds.contains(iu.getId())) {
it.remove();
}
}
}
/**
* Verifies that we found what we were looking for: it's possible that we have connector descriptors that are no
* longer available on their respective sites. In that case we must inform the user. Unfortunately this is the
* earliest point at which we can know.
*/
private void checkForUnavailable(final List<IInstallableUnit> installableUnits) throws CoreException {
// at least one selected connector could not be found in a repository
Set<String> foundIds = new HashSet<String>();
for (IInstallableUnit unit : installableUnits) {
foundIds.add(unit.getId());
}
Set<String> installFeatureIds = new HashSet<String>();
for (FeatureEntry entry : featureEntries) {
Operation operation = entry.computeChangeOperation();
if (operation == Operation.INSTALL || operation == Operation.UPDATE) {
installFeatureIds.add(entry.getFeatureDescriptor().getId());
}
}
String message = ""; //$NON-NLS-1$
String detailedMessage = ""; //$NON-NLS-1$
for (CatalogItem descriptor : items) {
StringBuilder unavailableIds = null;
for (String id : getFeatureIds(descriptor)) {
if (!foundIds.contains(id) && installFeatureIds.contains(id)) {
if (unavailableIds == null) {
unavailableIds = new StringBuilder();
} else {
unavailableIds.append(Messages.ProvisioningOperation_commaSeparator);
}
unavailableIds.append(id);
}
}
if (unavailableIds != null) {
if (message.length() > 0) {
message += Messages.ProvisioningOperation_commaSeparator;
}
message += descriptor.getName();
if (detailedMessage.length() > 0) {
detailedMessage += Messages.ProvisioningOperation_commaSeparator;
}
detailedMessage += NLS.bind(Messages.ProvisioningOperation_unavailableFeatures, new Object[] {
descriptor.getName(), unavailableIds.toString(), descriptor.getSiteUrl() });
}
}
if (message.length() > 0) {
// instead of aborting here we ask the user if they wish to proceed anyways
final boolean[] okayToProceed = new boolean[1];
final String finalMessage = message;
Display.getDefault().syncExec(new Runnable() {
public void run() {
okayToProceed[0] = MessageDialog.openQuestion(WorkbenchUtil.getShell(),
Messages.ProvisioningOperation_proceedQuestion, NLS.bind(
Messages.ProvisioningOperation_unavailableSolutions_proceedQuestion,
new Object[] { finalMessage }));
}
});
if (!okayToProceed[0]) {
throw new CoreException(new Status(IStatus.ERROR, MarketplaceClientUi.BUNDLE_ID, NLS.bind(
Messages.ProvisioningOperation_unavailableSolutions, detailedMessage), null));
}
}
}
}