blob: 68c69ea2578b9a4bce6fcdd3f6ea5a6033084a4a [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2012, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.r.core.pkgmanager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.rj.renv.core.RNumVersion;
import org.eclipse.statet.rj.renv.core.RPkg;
public class RPkgResolver {
// R supports only >= for dependencies
// to support more operators, see e.g.
// org.eclipse.equinox.internal.p2.director.Projector
// org.sat4j.pb.SolverFactory#newEclipseP2()
private static class RPkgActionVersionIterator implements Iterator<RNumVersion> {
private final List<? extends RPkgAction> list;
private int idx= 0;
public RPkgActionVersionIterator(final List<? extends RPkgAction> list) {
this.list= list;
}
@Override
public boolean hasNext() {
return (this.idx < this.list.size());
}
@Override
public RNumVersion next() {
return this.list.get(this.idx++).getPkg().getVersion();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private static class RPkgVersionIterator implements Iterator<RNumVersion> {
private final List<? extends RPkg> list;
private int idx= 0;
public RPkgVersionIterator(final List<? extends RPkg> list) {
this.list= list;
}
@Override
public boolean hasNext() {
return (this.idx < this.list.size());
}
@Override
public RNumVersion next() {
return this.list.get(this.idx++).getVersion();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private static void removeInvalid(final RPkg reqPkg, final List<? extends RPkg> availablePkgs) {
final RNumVersion reqVersion= reqPkg.getVersion();
if (reqVersion.toString().startsWith(">=")) {
for (final Iterator<? extends RPkg> iter= availablePkgs.iterator(); iter.hasNext();) {
if (!reqVersion.isSatisfiedBy(iter.next().getVersion())) {
iter.remove();
}
}
}
}
private class Context {
Context() {
}
List<? extends IRPkgData> getRequired(final String name) {
return RPkgResolver.this.required.get(name);
}
void setRequired(final String name, final List<? extends IRPkgData> list) {
RPkgResolver.this.required.put(name, list);
}
void setRequiredMissing(final String name) {
setRequired(name, Collections.<IRPkgData>emptyList());
}
void handleProblem(final int severity, final String message, final String... args) {
RPkgResolver.this.statusList.add(new Status(severity, RCore.BUNDLE_ID,
(args != null && args.length > 0) ? NLS.bind(message, args) : message ));
}
}
private class TmpContext extends Context {
private final Map<String, List<? extends IRPkgData>> tmpRequired= new HashMap<>();
TmpContext() {
}
@Override
List<? extends IRPkgData> getRequired(final String name) {
final List<? extends IRPkgData> list= this.tmpRequired.get(name);
return (list != null) ? list : super.getRequired(name);
}
@Override
void setRequired(final String name, final List<? extends IRPkgData> list) {
this.tmpRequired.put(name, list);
}
@Override
void handleProblem(final int severity, final String message, final String... args) {
throw new OperationCanceledException();
}
Set<String> getTmpNames() {
return this.tmpRequired.keySet();
}
void merge() {
RPkgResolver.this.required.putAll(this.tmpRequired);
}
void reset() {
this.tmpRequired.clear();
}
}
/**
* Package set from package manager.
*/
private final IRPkgSet.Ext pkgSet;
/**
* Packages requested for installation.
**/
private final Map<String, List<RPkgAction.Install>> selected;
/**
* Sorted list of names of packages requested for installation.
**/
private final List<String> selectedNames;
/**
* Packages required for installation of the request (but not selected).
* The list for each required package contains all package versions valid for the installation.
* The first is finally installed. If empty, no valid package version is available.
**/
private final Map<String, List<? extends IRPkgData>> required;
/**
* If required packages should be added.
*/
private boolean addRequired;
/**
* Set of names of {@link #required required packages} which need to be checked.
*/
private final LinkedHashSet<String> requiredToCheck;
/**
* If the package is only suggested.
*/
private Set<String> suggested;
private final List<IStatus> statusList;
private IStatus status;
public RPkgResolver(final IRPkgSet.Ext pkgSet, final Map<String, List<RPkgAction.Install>> pkgs) {
this.pkgSet= pkgSet;
this.selected= pkgs;
final String[] names= pkgs.keySet().toArray(new String[pkgs.size()]);
Arrays.sort(names);
this.selectedNames= ImCollections.newList(names);
this.required= new IdentityHashMap<>();
this.addRequired= true;
this.requiredToCheck= new LinkedHashSet<>(16);
this.statusList= new ArrayList<>();
}
public void setAddSuggested(final boolean enabled) {
if (this.status != null) {
throw new IllegalStateException();
}
this.suggested= (enabled) ? new HashSet<>(8) : null;
}
public void setAddRequired(final boolean enabled) {
if (this.status != null) {
throw new IllegalStateException();
}
this.addRequired= enabled;
}
public IStatus run() {
resolve();
if (this.statusList.isEmpty()) {
this.status= Status.OK_STATUS;
}
else {
this.status= new MultiStatus(RCore.BUNDLE_ID, 0,
this.statusList.toArray(new IStatus[this.statusList.size()]),
"Cannot install the selected packages.", null );
}
return this.status;
}
public IStatus getStatus() {
if (this.status == null) {
throw new IllegalStateException();
}
return this.status;
}
// public boolean isSelected(final String name) {
// return this.selected.containsKey(name);
// }
//
// public boolean isRequired(final String name) {
// return this.required.containsKey(name)
// && (this.suggested == null || !this.suggested.contains(name));
// }
//
// public boolean isSuggested(final String name) {
// return (this.suggested != null && this.suggested.contains(name));
// }
public String getReason(final RPkg pkg) {
final String name= pkg.getName();
if (this.selected.containsKey(name)) {
return "selected";
}
if (this.suggested != null && this.suggested.contains(name)) {
return "suggested";
}
if (this.required.containsKey(name)) {
return "required";
}
return null;
}
private void resolve() {
final Context main= new Context();
if (this.addRequired) {
for (final String name : this.selectedNames) {
final List<? extends RPkgAction.Install> list= this.selected.get(name);
for (final RPkgAction.Install action : list) {
final IRPkgData pkg= action.getPkg();
check(pkg, "selected", "depends on", pkg.getDepends(), main);
check(pkg, "selected", "imports", pkg.getImports(), main);
check(pkg, "selected", "is linking to", pkg.getLinkingTo(), main);
}
}
if (!this.statusList.isEmpty()) {
return;
}
checkRequired(main);
if (!this.statusList.isEmpty()) {
return;
}
final TmpContext tmp= new TmpContext();
if (this.suggested != null) {
for (final String name : this.selectedNames) {
final List<? extends RPkgAction.Install> list= this.selected.get(name);
for (final RPkgAction.Install action : list) {
final IRPkgData pkg= action.getPkg();
try {
check(pkg, "selected", "suggests", pkg.getSuggests(), tmp);
checkRequired(tmp);
this.suggested.addAll(tmp.getTmpNames());
tmp.merge();
}
catch (final OperationCanceledException e) {
// e.printStackTrace();
}
finally {
this.requiredToCheck.clear();
}
tmp.reset();
}
}
}
}
}
private void checkRequired(final Context context) {
Iterator<String> iter= this.requiredToCheck.iterator();
while (iter.hasNext()) {
final String pkgName= iter.next();
iter.remove();
final List<? extends IRPkgData> list= context.getRequired(pkgName);
if (list != null && !list.isEmpty()) {
final IRPkgData pkg= list.get(0);
check(pkg, "required", "depends on", pkg.getDepends(), context);
check(pkg, "required", "imports", pkg.getImports(), context);
check(pkg, "required", "is linking to", pkg.getLinkingTo(), context);
iter= this.requiredToCheck.iterator();
}
}
}
private void check(final RPkg pkg, final String pkgLabel, final String reqLabel,
final List<? extends RPkg> reqList, final Context context) {
if (pkg.getName().equals("R")) {
return;
}
for (final RPkg reqPkg : reqList) {
final String reqName= reqPkg.getName();
final RNumVersion reqVersion= reqPkg.getVersion();
if (reqName.equals("R")) {
continue;
}
{ final List<? extends RPkgAction.Install> selected= this.selected.get(reqName);
if (selected != null) {
if (!(reqVersion == RNumVersion.NONE || reqVersion.isSatisfiedByAny(
new RPkgActionVersionIterator(selected) ))) {
context.handleProblem(IStatus.ERROR, NLS.bind((pkgLabel == "selected") ?
"The selected packages ''{1}'' ({2}) and ''{3}'' ({4}) are not compatible, ''{1}'' {5} version {6} of ''{3}''." :
"The {0} package ''{1}'' ({2}) and the selected package ''{3}'' ({4}) are not compatible, ''{1}'' {5} version {6} of ''{3}''.",
new Object[] { pkgLabel, pkg.getName(), pkg.getVersion(),
reqName, selected.get(0).getPkg().getVersion(), reqLabel,
reqVersion }));
continue;
}
continue;
}
}
if (isReqInstalled(reqName, reqVersion)) {
continue;
}
{ List<? extends IRPkgData> list= context.getRequired(reqName);
if (list != null && list.isEmpty()) {
continue; // already reported
}
IRPkgData old;
if (list == null) {
list= this.pkgSet.getAvailable().get(reqName);
old= null;
}
else {
old= list.get(0);
}
if (list == null || list.isEmpty()) {
context.setRequiredMissing(reqName);
context.handleProblem(IStatus.ERROR, NLS.bind(
"The {0} package ''{1}'' ({2}) {4} package ''{3}'', but no version of ''{3}'' can be found.",
new Object[] { pkgLabel, pkg.getName(), pkg.getVersion(),
reqName, reqLabel }));
continue;
}
removeInvalid(reqPkg, list);
if (list.isEmpty()) {
context.setRequiredMissing(reqName);
if (old != null) {
this.requiredToCheck.remove(reqName);
}
context.handleProblem(IStatus.ERROR, NLS.bind(
"The {0} package ''{1}'' ({2}) {4} version {5} of package ''{3}'', but no compatible version of ''{3}'' can be found.",
new Object[] { pkgLabel, pkg.getName(), pkg.getVersion(),
reqName, reqLabel, reqVersion }));
continue;
}
else {
context.setRequired(reqName, list);
if (old != list.get(0)) {
this.requiredToCheck.add(reqName);
}
continue;
}
}
}
}
private boolean isReqInstalled(final String reqName, final RNumVersion reqVersion) {
final List<? extends IRPkgData> list= this.pkgSet.getInstalled().get(reqName);
return (!list.isEmpty()
&& (reqVersion == RNumVersion.NONE || reqVersion.isSatisfiedByAny(
new RPkgVersionIterator(list) )));
}
private class ActionCollector {
private final Set<String> visited;
private final List<RPkgAction.Install> ordered;
public ActionCollector() {
final int count= RPkgResolver.this.selected.size() + RPkgResolver.this.required.size();
this.visited= new HashSet<>(count);
this.ordered= new ArrayList<>(count);
}
public void run() {
for (final String name : RPkgResolver.this.selectedNames) {
addPkg(name);
}
// only required if (suggested != null)
final Set<String> keySet= RPkgResolver.this.required.keySet();
final String[] names= keySet.toArray(new String[keySet.size()]);
Arrays.sort(names);
for (final String name : names) {
addPkg(name);
}
}
private List<RPkgAction.Install> getFinal(final String name) {
{ final List<RPkgAction.Install> selected= RPkgResolver.this.selected.get(name);
if (selected != null) {
return selected;
}
}
{ final List<? extends IRPkgData> list= RPkgResolver.this.required.get(name);
if (list != null && !list.isEmpty()) {
return Collections.singletonList(
new RPkgAction.Install(list.get(0), null, null) );
}
}
return Collections.emptyList();
}
private void addPkg(final String pkgName) {
if (this.visited.add(pkgName)) {
final List<RPkgAction.Install> actions= getFinal(pkgName);
for (final RPkgAction.Install action : actions) {
final IRPkgData pkg= action.getPkg();
addReqPkgs(pkg.getDepends());
addReqPkgs(pkg.getImports());
addReqPkgs(pkg.getLinkingTo());
this.ordered.add(action);
}
}
}
private void addReqPkgs(final List<? extends RPkg> pkgs) {
for (final RPkg pkg : pkgs) {
if (isReqInstalled(pkg.getName(), pkg.getVersion())) {
continue; // later
}
addPkg(pkg.getName());
}
}
public List<RPkgAction.Install> getActions() {
return this.ordered;
}
}
public List<RPkgAction.Install> createActions() {
final ActionCollector collector= new ActionCollector();
collector.run();
return collector.getActions();
}
}