1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.util.graph.transformer;
20
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.HashSet;
25 import java.util.Iterator;
26 import java.util.Objects;
27 import java.util.Set;
28 import java.util.stream.Collectors;
29
30 import org.eclipse.aether.ConfigurationProperties;
31 import org.eclipse.aether.RepositoryException;
32 import org.eclipse.aether.RepositorySystemSession;
33 import org.eclipse.aether.collection.DependencyGraphTransformationContext;
34 import org.eclipse.aether.collection.UnsolvableVersionConflictException;
35 import org.eclipse.aether.graph.DependencyFilter;
36 import org.eclipse.aether.graph.DependencyNode;
37 import org.eclipse.aether.util.ConfigUtils;
38 import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext;
39 import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem;
40 import org.eclipse.aether.util.graph.transformer.ConflictResolver.VersionSelector;
41 import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
42 import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
43 import org.eclipse.aether.version.Version;
44 import org.eclipse.aether.version.VersionConstraint;
45
46 import static java.util.Objects.requireNonNull;
47
48
49
50
51
52
53
54
55 public class ConfigurableVersionSelector extends VersionSelector {
56
57
58
59
60
61
62
63
64 public static final String CONFIG_PROP_SELECTION_STRATEGY =
65 ConfigurationProperties.PREFIX_AETHER + "conflictResolver.versionSelector.selectionStrategy";
66
67 public static final String NEAREST_SELECTION_STRATEGY = "nearest";
68 public static final String HIGHEST_SELECTION_STRATEGY = "highest";
69
70 public static final String DEFAULT_SELECTION_STRATEGY = NEAREST_SELECTION_STRATEGY;
71
72
73
74
75 public interface SelectionStrategy {
76
77
78
79
80
81
82 boolean isBetter(ConflictItem candidate, ConflictItem winner);
83
84
85
86
87
88
89
90
91 default ConflictItem winnerSelected(
92 ConflictItem winner, Collection<ConflictItem> candidates, ConflictContext context)
93 throws UnsolvableVersionConflictException {
94 return winner;
95 }
96 }
97
98
99
100
101 protected final SelectionStrategy selectionStrategy;
102
103
104
105
106 public ConfigurableVersionSelector() {
107 this.selectionStrategy = null;
108 }
109
110
111
112
113
114
115
116 public ConfigurableVersionSelector(SelectionStrategy selectionStrategy) {
117 this.selectionStrategy = requireNonNull(selectionStrategy, "selectionStrategy");
118 }
119
120 @Override
121 public VersionSelector getInstance(DependencyNode root, DependencyGraphTransformationContext context)
122 throws RepositoryException {
123 if (selectionStrategy == null) {
124 String ss = ConfigUtils.getString(
125 context.getSession(), DEFAULT_SELECTION_STRATEGY, CONFIG_PROP_SELECTION_STRATEGY);
126 SelectionStrategy strategy;
127 if (NEAREST_SELECTION_STRATEGY.equals(ss)) {
128 strategy = new Nearest();
129 } else if (HIGHEST_SELECTION_STRATEGY.equals(ss)) {
130 strategy = new Highest();
131 } else {
132 throw new IllegalArgumentException("Unknown selection strategy: " + ss + "; known are "
133 + Arrays.asList(NEAREST_SELECTION_STRATEGY, HIGHEST_SELECTION_STRATEGY));
134 }
135 return new ConfigurableVersionSelector(strategy);
136 } else {
137 return this;
138 }
139 }
140
141 @Override
142 public void selectVersion(ConflictContext context) throws RepositoryException {
143 ConflictGroup group = new ConflictGroup();
144 for (ConflictItem candidate : context.getItems()) {
145 DependencyNode node = candidate.getNode();
146 VersionConstraint constraint = node.getVersionConstraint();
147
148 boolean backtrack = false;
149 boolean hardConstraint = constraint.getRange() != null;
150
151 if (hardConstraint) {
152 if (group.constraints.add(constraint)) {
153 if (group.winner != null
154 && !constraint.containsVersion(
155 group.winner.getNode().getVersion())) {
156 backtrack = true;
157 }
158 }
159 }
160
161 if (isAcceptableByConstraints(group, node.getVersion())) {
162 group.candidates.add(candidate);
163
164 if (backtrack) {
165 backtrack(group, context);
166 } else if (group.winner == null || selectionStrategy.isBetter(candidate, group.winner)) {
167 group.winner = candidate;
168 }
169 } else if (backtrack) {
170 backtrack(group, context);
171 }
172 }
173 context.setWinner(selectionStrategy.winnerSelected(group.winner, group.candidates, context));
174 }
175
176 protected void backtrack(ConflictGroup group, ConflictContext context) throws UnsolvableVersionConflictException {
177 group.winner = null;
178
179 for (Iterator<ConflictItem> it = group.candidates.iterator(); it.hasNext(); ) {
180 ConflictItem candidate = it.next();
181
182 if (!isAcceptableByConstraints(group, candidate.getNode().getVersion())) {
183 it.remove();
184 } else if (group.winner == null || selectionStrategy.isBetter(candidate, group.winner)) {
185 group.winner = candidate;
186 }
187 }
188
189 if (group.winner == null) {
190 throw newFailure("Unsolvable hard constraint combination", context);
191 }
192 }
193
194 protected boolean isAcceptableByConstraints(ConflictGroup group, Version version) {
195 for (VersionConstraint constraint : group.constraints) {
196 if (!constraint.containsVersion(version)) {
197 return false;
198 }
199 }
200 return true;
201 }
202
203 @Override
204 public String toString() {
205 return getClass().getSimpleName() + "("
206 + (selectionStrategy != null ? selectionStrategy.getClass().getSimpleName() : "not inited") + ")";
207 }
208
209
210
211
212 public static UnsolvableVersionConflictException newFailure(String message, ConflictContext context) {
213 DependencyFilter filter = (node, parents) -> {
214 requireNonNull(node, "node cannot be null");
215 requireNonNull(parents, "parents cannot be null");
216 return context.isIncluded(node);
217 };
218 PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor(filter);
219 context.getRoot().accept(new TreeDependencyVisitor(visitor));
220 return new UnsolvableVersionConflictException(message, visitor.getPaths());
221 }
222
223 protected static class ConflictGroup {
224
225 final Collection<VersionConstraint> constraints;
226
227 final Collection<ConflictItem> candidates;
228
229 ConflictItem winner;
230
231 ConflictGroup() {
232 constraints = new HashSet<>();
233 candidates = new ArrayList<>(64);
234 }
235
236 @Override
237 public String toString() {
238 return String.valueOf(winner);
239 }
240 }
241
242
243
244
245
246
247
248
249
250 public static class Nearest implements SelectionStrategy {
251 @Override
252 public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
253 if (candidate.isSibling(winner)) {
254 return candidate
255 .getNode()
256 .getVersion()
257 .compareTo(winner.getNode().getVersion())
258 > 0;
259 } else {
260 return candidate.getDepth() < winner.getDepth();
261 }
262 }
263 }
264
265
266
267
268
269
270
271
272
273 public static class Highest implements SelectionStrategy {
274 @Override
275 public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
276 if (winner.getDepth() <= 1) {
277 return false;
278 } else if (candidate.getDepth() <= 1) {
279 return true;
280 } else if (candidate.getNode().getVersion().equals(winner.getNode().getVersion())) {
281 return candidate.getDepth() < winner.getDepth();
282 } else {
283 return candidate
284 .getNode()
285 .getVersion()
286 .compareTo(winner.getNode().getVersion())
287 > 0;
288 }
289 }
290 }
291
292
293
294
295
296
297
298 public static class VersionConvergence implements SelectionStrategy {
299 private final SelectionStrategy delegate;
300
301 public VersionConvergence(SelectionStrategy delegate) {
302 this.delegate = requireNonNull(delegate, "delegate");
303 }
304
305 @Override
306 public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
307 return delegate.isBetter(candidate, winner);
308 }
309
310 @Override
311 public ConflictItem winnerSelected(
312 ConflictItem winner, Collection<ConflictItem> candidates, ConflictContext context)
313 throws UnsolvableVersionConflictException {
314 if (winner != null && winner.getNode().getVersionConstraint().getRange() == null) {
315 Set<String> versions = candidates.stream()
316 .map(c -> c.getDependency().getArtifact().getVersion())
317 .collect(Collectors.toSet());
318 if (versions.size() > 1) {
319 throw newFailure(
320 "Convergence violated for "
321 + winner.getDependency().getArtifact().getGroupId() + ":"
322 + winner.getDependency().getArtifact().getArtifactId() + ", versions present: "
323 + versions,
324 context);
325 }
326 }
327 return winner;
328 }
329 }
330
331
332
333
334
335
336
337 public static class MajorVersionConvergence implements SelectionStrategy {
338 private final SelectionStrategy delegate;
339
340 public MajorVersionConvergence(SelectionStrategy delegate) {
341 this.delegate = requireNonNull(delegate, "delegate");
342 }
343
344 @Override
345 public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
346 return delegate.isBetter(candidate, winner);
347 }
348
349 @Override
350 public ConflictItem winnerSelected(
351 ConflictItem winner, Collection<ConflictItem> candidates, ConflictContext context)
352 throws UnsolvableVersionConflictException {
353 if (winner != null && !candidates.isEmpty()) {
354 Set<String> incompatibleVersions = candidates.stream()
355 .filter(c -> !sameMajor(c, winner))
356 .map(c -> c.getDependency().getArtifact().getVersion())
357 .collect(Collectors.toSet());
358 if (!incompatibleVersions.isEmpty()) {
359 Set<String> allVersions = candidates.stream()
360 .map(c -> c.getDependency().getArtifact().getVersion())
361 .collect(Collectors.toSet());
362 throw newFailure(
363 "Incompatible versions for "
364 + winner.getDependency().getArtifact().getGroupId() + ":"
365 + winner.getDependency().getArtifact().getArtifactId() + ", incompatible versions: "
366 + incompatibleVersions + ", all versions " + allVersions,
367 context);
368 }
369 }
370 return winner;
371 }
372
373 private boolean sameMajor(ConflictItem candidate, ConflictItem winner) {
374 String candidateVersion = candidate.getDependency().getArtifact().getVersion();
375 String winnerVersion = winner.getDependency().getArtifact().getVersion();
376
377 if (candidateVersion.contains(".") && winnerVersion.contains(".")) {
378 String candidateMajor = candidateVersion.substring(0, candidateVersion.indexOf('.'));
379 String winnerMajor = winnerVersion.substring(0, winnerVersion.indexOf('.'));
380 return Objects.equals(candidateMajor, winnerMajor);
381 }
382 return true;
383 }
384 }
385 }