1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.eclipse.aether.util.graph.transformer;
20
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.IdentityHashMap;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.ListIterator;
30 import java.util.Map;
31 import java.util.Objects;
32
33 import org.eclipse.aether.RepositoryException;
34 import org.eclipse.aether.artifact.Artifact;
35 import org.eclipse.aether.collection.DependencyGraphTransformationContext;
36 import org.eclipse.aether.graph.DefaultDependencyNode;
37 import org.eclipse.aether.graph.Dependency;
38 import org.eclipse.aether.graph.DependencyNode;
39 import org.eclipse.aether.util.artifact.ArtifactIdUtils;
40
41 import static java.util.Objects.requireNonNull;
42
43 /**
44 * A legacy dependency graph transformer that resolves version and scope conflicts among dependencies.
45 * This implementation maintains backward compatibility with Maven 3.x and Resolver 1.x behavior but has
46 * O(N²) worst-case performance characteristics. For new projects, consider using {@link PathConflictResolver}.
47 * <p>
48 * For a given set of conflicting nodes, one node will be chosen as the winner. How losing nodes are handled
49 * depends on the configured verbosity level: they may be removed entirely, have their children removed, or
50 * be left in place with conflict information. The exact rules by which a winning node and its effective scope
51 * are determined are controlled by user-supplied implementations of {@link ConflictResolver.VersionSelector}, {@link ConflictResolver.ScopeSelector},
52 * {@link ConflictResolver.OptionalitySelector} and {@link ConflictResolver.ScopeDeriver}.
53 * <p>
54 * <strong>Performance Characteristics:</strong>
55 * <ul>
56 * <li><strong>Time Complexity:</strong> O(N²) worst-case where N is the number of dependency nodes</li>
57 * <li><strong>Memory Usage:</strong> Modifies the dependency graph in-place</li>
58 * <li><strong>Scalability:</strong> Performance degrades significantly on large multi-module projects</li>
59 * </ul>
60 * <p>
61 * <strong>Algorithm Overview:</strong>
62 * <ol>
63 * <li><strong>Depth-First Traversal:</strong> Walks the dependency graph depth-first</li>
64 * <li><strong>In-Place Modification:</strong> Modifies nodes directly during traversal</li>
65 * <li><strong>Conflict Detection:</strong> Identifies conflicts by comparing conflict IDs</li>
66 * <li><strong>Winner Selection:</strong> Uses provided selectors to choose winners</li>
67 * <li><strong>Loser Removal:</strong> Removes or marks losing nodes during traversal</li>
68 * </ol>
69 * <p>
70 * <strong>When to Use:</strong>
71 * <ul>
72 * <li>Exact backward compatibility with Maven 3.x/Resolver 1.x is required</li>
73 * <li>Debugging performance differences between old and new algorithms</li>
74 * <li>Small projects where performance is not a concern</li>
75 * <li>Testing and validation scenarios</li>
76 * </ul>
77 * <p>
78 * <strong>Migration Recommendation:</strong> New projects should use {@link PathConflictResolver} for better
79 * performance. This implementation is retained primarily for compatibility and testing purposes.
80 * <p>
81 * <strong>Implementation Note:</strong> This conflict resolver is identical to the one used in Maven 3/Resolver 1.x.
82 * The implementation may produce O(N²) worst-case performance on projects with many small conflict groups
83 * (typically one member each), which is common in large multi-module projects.
84 *
85 * @see PathConflictResolver
86 * @since 2.0.11
87 */
88 public final class ClassicConflictResolver extends ConflictResolver {
89 private final ConflictResolver.VersionSelector versionSelector;
90 private final ConflictResolver.ScopeSelector scopeSelector;
91 private final ConflictResolver.ScopeDeriver scopeDeriver;
92 private final ConflictResolver.OptionalitySelector optionalitySelector;
93
94 /**
95 * Creates a new conflict resolver instance with the specified hooks.
96 *
97 * @param versionSelector the version selector to use, must not be {@code null}
98 * @param scopeSelector the scope selector to use, must not be {@code null}
99 * @param optionalitySelector the optionality selector ot use, must not be {@code null}
100 * @param scopeDeriver the scope deriver to use, must not be {@code null}
101 */
102 public ClassicConflictResolver(
103 ConflictResolver.VersionSelector versionSelector,
104 ConflictResolver.ScopeSelector scopeSelector,
105 ConflictResolver.OptionalitySelector optionalitySelector,
106 ConflictResolver.ScopeDeriver scopeDeriver) {
107 this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null");
108 this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null");
109 this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null");
110 this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null");
111 }
112
113 @SuppressWarnings("unchecked")
114 @Override
115 public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
116 throws RepositoryException {
117 requireNonNull(node, "node cannot be null");
118 requireNonNull(context, "context cannot be null");
119 List<String> sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
120 if (sortedConflictIds == null) {
121 ConflictIdSorter sorter = new ConflictIdSorter();
122 sorter.transformGraph(node, context);
123
124 sortedConflictIds = (List<String>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
125 }
126
127 @SuppressWarnings("unchecked")
128 Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
129 long time1 = System.nanoTime();
130
131 @SuppressWarnings("unchecked")
132 Collection<Collection<String>> conflictIdCycles =
133 (Collection<Collection<String>>) context.get(TransformationContextKeys.CYCLIC_CONFLICT_IDS);
134 if (conflictIdCycles == null) {
135 throw new RepositoryException("conflict id cycles have not been identified");
136 }
137
138 Map<DependencyNode, String> conflictIds =
139 (Map<DependencyNode, String>) context.get(TransformationContextKeys.CONFLICT_IDS);
140 if (conflictIds == null) {
141 throw new RepositoryException("conflict groups have not been identified");
142 }
143
144 Map<String, Collection<String>> cyclicPredecessors = new HashMap<>();
145 for (Collection<String> cycle : conflictIdCycles) {
146 for (String conflictId : cycle) {
147 Collection<String> predecessors = cyclicPredecessors.computeIfAbsent(conflictId, k -> new HashSet<>());
148 predecessors.addAll(cycle);
149 }
150 }
151
152 State state = new State(node, conflictIds, sortedConflictIds.size(), context);
153 for (Iterator<String> it = sortedConflictIds.iterator(); it.hasNext(); ) {
154 String conflictId = it.next();
155
156 // reset data structures for next graph walk
157 state.prepare(conflictId, cyclicPredecessors.get(conflictId));
158
159 // find nodes with the current conflict id and while walking the graph (more deeply), nuke leftover losers
160 gatherConflictItems(node, state);
161
162 // now that we know the min depth of the parents, update depth of conflict items
163 state.finish();
164
165 // earlier runs might have nuked all parents of the current conflict id, so it might not exist anymore
166 if (!state.items.isEmpty()) {
167 ConflictContext ctx = state.conflictCtx;
168 state.versionSelector.selectVersion(ctx);
169 if (ctx.winner == null) {
170 throw new RepositoryException("conflict resolver did not select winner among " + state.items);
171 }
172 DependencyNode winner = ((ConflictItem) (ctx.winner)).node;
173
174 state.scopeSelector.selectScope(ctx);
175 if (ConflictResolver.Verbosity.NONE != state.verbosity) {
176 winner.setData(
177 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
178 winner.getDependency().getScope());
179 }
180 winner.setScope(ctx.scope);
181
182 state.optionalitySelector.selectOptionality(ctx);
183 if (ConflictResolver.Verbosity.NONE != state.verbosity) {
184 winner.setData(
185 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
186 winner.getDependency().isOptional());
187 }
188 winner.setOptional(ctx.optional);
189
190 removeLosers(state);
191 }
192
193 // record the winner so we can detect leftover losers during future graph walks
194 state.winner();
195
196 // in case of cycles, trigger final graph walk to ensure all leftover losers are gone
197 if (!it.hasNext() && !conflictIdCycles.isEmpty() && state.conflictCtx.winner != null) {
198 DependencyNode winner = ((ConflictItem) (state.conflictCtx).winner).node;
199 // Note: using non-existing key here (empty) as that one for sure was not met
200 state.prepare("", null);
201 gatherConflictItems(winner, state);
202 }
203 }
204
205 if (stats != null) {
206 long time2 = System.nanoTime();
207 stats.put("ConflictResolver.totalTime", time2 - time1);
208 stats.put("ConflictResolver.conflictItemCount", state.totalConflictItems);
209 }
210
211 return node;
212 }
213
214 private boolean gatherConflictItems(DependencyNode node, State state) throws RepositoryException {
215 String conflictId = state.conflictIds.get(node);
216 if (state.currentId.equals(conflictId)) {
217 // found it, add conflict item (if not already done earlier by another path)
218 state.add(node);
219 // we don't recurse here so we might miss losers beneath us, those will be nuked during future walks below
220 } else if (state.loser(node, conflictId)) {
221 // found a leftover loser (likely in a cycle) of an already processed conflict id, tell caller to nuke it
222 return false;
223 } else if (state.push(node, conflictId)) {
224 // found potential parent, no cycle and not visited before with the same derived scope, so recurse
225 for (Iterator<DependencyNode> it = node.getChildren().iterator(); it.hasNext(); ) {
226 DependencyNode child = it.next();
227 if (!gatherConflictItems(child, state)) {
228 it.remove();
229 }
230 }
231 state.pop();
232 }
233 return true;
234 }
235
236 private static void removeLosers(State state) {
237 ConflictItem winner = ((ConflictItem) state.conflictCtx.winner);
238 String winnerArtifactId = ArtifactIdUtils.toId(winner.node.getArtifact());
239 List<DependencyNode> previousParent = null;
240 ListIterator<DependencyNode> childIt = null;
241 HashSet<String> toRemoveIds = new HashSet<>();
242 for (ConflictItem item : state.items) {
243 if (item == winner) {
244 continue;
245 }
246 if (item.parent != previousParent) {
247 childIt = item.parent.listIterator();
248 previousParent = item.parent;
249 }
250 while (childIt.hasNext()) {
251 DependencyNode child = childIt.next();
252 if (child == item.node) {
253 // NONE: just remove it and done
254 if (ConflictResolver.Verbosity.NONE == state.verbosity) {
255 childIt.remove();
256 break;
257 }
258
259 // STANDARD: doing extra bookkeeping to select "which nodes to remove"
260 if (ConflictResolver.Verbosity.STANDARD == state.verbosity) {
261 String childArtifactId = ArtifactIdUtils.toId(child.getArtifact());
262 // if two IDs are equal, it means "there is nearest", not conflict per se.
263 // In that case we do NOT allow this child to be removed (but remove others)
264 // and this keeps us safe from iteration (and in general, version) ordering
265 // as we explicitly leave out ID that is "nearest found" state.
266 //
267 // This tackles version ranges mostly, where ranges are turned into list of
268 // several nodes in collector (as many were discovered, ie. from metadata), and
269 // old code would just "mark" the first hit as conflict, and remove the rest,
270 // even if rest could contain "more suitable" version, that is not conflicting/diverging.
271 // This resulted in verbose mode transformed tree, that was misrepresenting things
272 // for dependency convergence calculations: it represented state like parent node
273 // depends on "wrong" version (diverge), while "right" version was present (but removed)
274 // as well, as it was contained in parents version range.
275 if (!Objects.equals(winnerArtifactId, childArtifactId)) {
276 toRemoveIds.add(childArtifactId);
277 }
278 }
279
280 // FULL: just record the facts
281 DependencyNode loser = new DefaultDependencyNode(child);
282 loser.setData(ConflictResolver.NODE_DATA_WINNER, winner.node);
283 loser.setData(
284 ConflictResolver.NODE_DATA_ORIGINAL_SCOPE,
285 loser.getDependency().getScope());
286 loser.setData(
287 ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY,
288 loser.getDependency().isOptional());
289 loser.setScope(item.getScopes().iterator().next());
290 loser.setChildren(Collections.emptyList());
291 childIt.set(loser);
292 item.node = loser;
293 break;
294 }
295 }
296 }
297
298 // 2nd pass to apply "standard" verbosity: leaving only 1 loser, but with care
299 if (ConflictResolver.Verbosity.STANDARD == state.verbosity && !toRemoveIds.isEmpty()) {
300 previousParent = null;
301 for (ConflictItem item : state.items) {
302 if (item == winner) {
303 continue;
304 }
305 if (item.parent != previousParent) {
306 childIt = item.parent.listIterator();
307 previousParent = item.parent;
308 }
309 while (childIt.hasNext()) {
310 DependencyNode child = childIt.next();
311 if (child == item.node) {
312 String childArtifactId = ArtifactIdUtils.toId(child.getArtifact());
313 if (toRemoveIds.contains(childArtifactId)
314 && relatedSiblingsCount(child.getArtifact(), item.parent) > 1) {
315 childIt.remove();
316 }
317 break;
318 }
319 }
320 }
321 }
322
323 // there might still be losers beneath the winner (e.g. in case of cycles)
324 // those will be nuked during future graph walks when we include the winner in the recursion
325 }
326
327 private static long relatedSiblingsCount(Artifact artifact, List<DependencyNode> parent) {
328 String ga = artifact.getGroupId() + ":" + artifact.getArtifactId();
329 return parent.stream()
330 .map(DependencyNode::getArtifact)
331 .filter(a -> ga.equals(a.getGroupId() + ":" + a.getArtifactId()))
332 .count();
333 }
334
335 static final class NodeInfo {
336
337 /**
338 * The smallest depth at which the node was seen, used for "the" depth of its conflict items.
339 */
340 int minDepth;
341
342 /**
343 * The set of derived scopes the node was visited with, used to check whether an already seen node needs to be
344 * revisited again in context of another scope. To conserve memory, we start with {@code String} and update to
345 * {@code Set<String>} if needed.
346 */
347 Object derivedScopes;
348
349 /**
350 * The set of derived optionalities the node was visited with, used to check whether an already seen node needs
351 * to be revisited again in context of another optionality. To conserve memory, encoded as bit field (bit 0 ->
352 * optional=false, bit 1 -> optional=true).
353 */
354 int derivedOptionalities;
355
356 /**
357 * The conflict items which are immediate children of the node, used to easily update those conflict items after
358 * a new parent scope/optionality was encountered.
359 */
360 List<ConflictItem> children;
361
362 static final int CHANGE_SCOPE = 0x01;
363
364 static final int CHANGE_OPTIONAL = 0x02;
365
366 private static final int OPT_FALSE = 0x01;
367
368 private static final int OPT_TRUE = 0x02;
369
370 NodeInfo(int depth, String derivedScope, boolean optional) {
371 minDepth = depth;
372 derivedScopes = derivedScope;
373 derivedOptionalities = optional ? OPT_TRUE : OPT_FALSE;
374 }
375
376 @SuppressWarnings("unchecked")
377 int update(int depth, String derivedScope, boolean optional) {
378 if (depth < minDepth) {
379 minDepth = depth;
380 }
381 int changes;
382 if (derivedScopes.equals(derivedScope)) {
383 changes = 0;
384 } else if (derivedScopes instanceof Collection) {
385 changes = ((Collection<String>) derivedScopes).add(derivedScope) ? CHANGE_SCOPE : 0;
386 } else {
387 Collection<String> scopes = new HashSet<>();
388 scopes.add((String) derivedScopes);
389 scopes.add(derivedScope);
390 derivedScopes = scopes;
391 changes = CHANGE_SCOPE;
392 }
393 int bit = optional ? OPT_TRUE : OPT_FALSE;
394 if ((derivedOptionalities & bit) == 0) {
395 derivedOptionalities |= bit;
396 changes |= CHANGE_OPTIONAL;
397 }
398 return changes;
399 }
400
401 void add(ConflictItem item) {
402 if (children == null) {
403 children = new ArrayList<>(1);
404 }
405 children.add(item);
406 }
407 }
408
409 final class State {
410
411 /**
412 * The conflict id currently processed.
413 */
414 String currentId;
415
416 /**
417 * Stats counter.
418 */
419 int totalConflictItems;
420
421 /**
422 * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts.
423 */
424 final ConflictResolver.Verbosity verbosity;
425
426 /**
427 * A mapping from conflict id to winner node, helps to recognize nodes that have their effective
428 * scope&optionality set or are leftovers from previous removals.
429 */
430 final Map<String, DependencyNode> resolvedIds;
431
432 /**
433 * The set of conflict ids which could apply to ancestors of nodes with the current conflict id, used to avoid
434 * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account
435 * for cyclic dependencies.
436 */
437 final Collection<String> potentialAncestorIds;
438
439 /**
440 * The output from the conflict marker.
441 */
442 final Map<DependencyNode, String> conflictIds;
443
444 /**
445 * The conflict items we have gathered so far for the current conflict id.
446 */
447 final List<ConflictItem> items;
448
449 /**
450 * The (conceptual) mapping from nodes to extra infos, technically keyed by the node's child list which better
451 * captures the identity of a node since we're basically concerned with effects towards children.
452 */
453 final Map<List<DependencyNode>, NodeInfo> infos;
454
455 /**
456 * The set of nodes on the DFS stack to detect cycles, technically keyed by the node's child list to match the
457 * dirty graph structure produced by the dependency collector for cycles.
458 */
459 final Map<List<DependencyNode>, Boolean> stack;
460
461 /**
462 * The stack of parent nodes.
463 */
464 final List<DependencyNode> parentNodes;
465
466 /**
467 * The stack of derived scopes for parent nodes.
468 */
469 final List<String> parentScopes;
470
471 /**
472 * The stack of derived optional flags for parent nodes.
473 */
474 final List<Boolean> parentOptionals;
475
476 /**
477 * The stack of node infos for parent nodes, may contain {@code null} which is used to disable creating new
478 * conflict items when visiting their parent again (conflict items are meant to be unique by parent-node combo).
479 */
480 final List<NodeInfo> parentInfos;
481
482 /**
483 * The conflict context passed to the version/scope/optionality selectors, updated as we move along rather than
484 * recreated to avoid tmp objects.
485 */
486 final ConflictContext conflictCtx;
487
488 /**
489 * The scope context passed to the scope deriver, updated as we move along rather than recreated to avoid tmp
490 * objects.
491 */
492 final ScopeContext scopeCtx;
493
494 /**
495 * The effective version selector, i.e. after initialization.
496 */
497 final ConflictResolver.VersionSelector versionSelector;
498
499 /**
500 * The effective scope selector, i.e. after initialization.
501 */
502 final ConflictResolver.ScopeSelector scopeSelector;
503
504 /**
505 * The effective scope deriver, i.e. after initialization.
506 */
507 final ConflictResolver.ScopeDeriver scopeDeriver;
508
509 /**
510 * The effective optionality selector, i.e. after initialization.
511 */
512 final ConflictResolver.OptionalitySelector optionalitySelector;
513
514 State(
515 DependencyNode root,
516 Map<DependencyNode, String> conflictIds,
517 int conflictIdCount,
518 DependencyGraphTransformationContext context)
519 throws RepositoryException {
520 this.conflictIds = conflictIds;
521 this.verbosity = ConflictResolver.getVerbosity(context.getSession());
522 potentialAncestorIds = new HashSet<>(conflictIdCount * 2);
523 resolvedIds = new HashMap<>(conflictIdCount * 2);
524 items = new ArrayList<>(256);
525 infos = new IdentityHashMap<>(64);
526 stack = new IdentityHashMap<>(64);
527 parentNodes = new ArrayList<>(64);
528 parentScopes = new ArrayList<>(64);
529 parentOptionals = new ArrayList<>(64);
530 parentInfos = new ArrayList<>(64);
531 conflictCtx = new ConflictContext(root, conflictIds, items);
532 scopeCtx = new ScopeContext(null, null);
533 versionSelector = ClassicConflictResolver.this.versionSelector.getInstance(root, context);
534 scopeSelector = ClassicConflictResolver.this.scopeSelector.getInstance(root, context);
535 scopeDeriver = ClassicConflictResolver.this.scopeDeriver.getInstance(root, context);
536 optionalitySelector = ClassicConflictResolver.this.optionalitySelector.getInstance(root, context);
537 }
538
539 void prepare(String conflictId, Collection<String> cyclicPredecessors) {
540 currentId = conflictId;
541 conflictCtx.conflictId = conflictId;
542 conflictCtx.winner = null;
543 conflictCtx.scope = null;
544 conflictCtx.optional = null;
545 items.clear();
546 infos.clear();
547 if (cyclicPredecessors != null) {
548 potentialAncestorIds.addAll(cyclicPredecessors);
549 }
550 }
551
552 void finish() {
553 List<DependencyNode> previousParent = null;
554 int previousDepth = 0;
555 totalConflictItems += items.size();
556 for (ListIterator<ConflictItem> iterator = items.listIterator(items.size()); iterator.hasPrevious(); ) {
557 ConflictItem item = iterator.previous();
558 if (item.parent == previousParent) {
559 item.depth = previousDepth;
560 } else if (item.parent != null) {
561 previousParent = item.parent;
562 NodeInfo info = infos.get(previousParent);
563 previousDepth = info.minDepth + 1;
564 item.depth = previousDepth;
565 }
566 }
567 potentialAncestorIds.add(currentId);
568 }
569
570 void winner() {
571 resolvedIds.put(currentId, (conflictCtx.winner != null) ? ((ConflictItem) conflictCtx.winner).node : null);
572 }
573
574 boolean loser(DependencyNode node, String conflictId) {
575 DependencyNode winner = resolvedIds.get(conflictId);
576 return winner != null && winner != node;
577 }
578
579 boolean push(DependencyNode node, String conflictId) throws RepositoryException {
580 if (conflictId == null) {
581 if (node.getDependency() != null) {
582 if (node.getData().get(ConflictResolver.NODE_DATA_WINNER) != null) {
583 return false;
584 }
585 throw new RepositoryException("missing conflict id for node " + node);
586 }
587 } else if (!potentialAncestorIds.contains(conflictId)) {
588 return false;
589 }
590
591 List<DependencyNode> graphNode = node.getChildren();
592 if (stack.put(graphNode, Boolean.TRUE) != null) {
593 return false;
594 }
595
596 int depth = depth();
597 String scope = deriveScope(node, conflictId);
598 boolean optional = deriveOptional(node, conflictId);
599 NodeInfo info = infos.get(graphNode);
600 if (info == null) {
601 info = new NodeInfo(depth, scope, optional);
602 infos.put(graphNode, info);
603 parentInfos.add(info);
604 parentNodes.add(node);
605 parentScopes.add(scope);
606 parentOptionals.add(optional);
607 } else {
608 int changes = info.update(depth, scope, optional);
609 if (changes == 0) {
610 stack.remove(graphNode);
611 return false;
612 }
613 parentInfos.add(null); // disable creating new conflict items, we update the existing ones below
614 parentNodes.add(node);
615 parentScopes.add(scope);
616 parentOptionals.add(optional);
617 if (info.children != null) {
618 if ((changes & NodeInfo.CHANGE_SCOPE) != 0) {
619 ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size());
620 while (itemIterator.hasPrevious()) {
621 ConflictItem item = itemIterator.previous();
622 String childScope = deriveScope(item.node, null);
623 item.addScope(childScope);
624 }
625 }
626 if ((changes & NodeInfo.CHANGE_OPTIONAL) != 0) {
627 ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size());
628 while (itemIterator.hasPrevious()) {
629 ConflictItem item = itemIterator.previous();
630 boolean childOptional = deriveOptional(item.node, null);
631 item.addOptional(childOptional);
632 }
633 }
634 }
635 }
636
637 return true;
638 }
639
640 void pop() {
641 int last = parentInfos.size() - 1;
642 parentInfos.remove(last);
643 parentScopes.remove(last);
644 parentOptionals.remove(last);
645 DependencyNode node = parentNodes.remove(last);
646 stack.remove(node.getChildren());
647 }
648
649 void add(DependencyNode node) throws RepositoryException {
650 DependencyNode parent = parent();
651 if (parent == null) {
652 ConflictItem item = newConflictItem(parent, node);
653 items.add(item);
654 } else {
655 NodeInfo info = parentInfos.get(parentInfos.size() - 1);
656 if (info != null) {
657 ConflictItem item = newConflictItem(parent, node);
658 info.add(item);
659 items.add(item);
660 }
661 }
662 }
663
664 private ConflictItem newConflictItem(DependencyNode parent, DependencyNode node) throws RepositoryException {
665 return new ConflictItem(parent, node, deriveScope(node, null), deriveOptional(node, null));
666 }
667
668 private int depth() {
669 return parentNodes.size();
670 }
671
672 private DependencyNode parent() {
673 int size = parentNodes.size();
674 return (size <= 0) ? null : parentNodes.get(size - 1);
675 }
676
677 private String deriveScope(DependencyNode node, String conflictId) throws RepositoryException {
678 if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0
679 || (conflictId != null && resolvedIds.containsKey(conflictId))) {
680 return scope(node.getDependency());
681 }
682
683 int depth = parentNodes.size();
684 scopes(depth, node.getDependency());
685 if (depth > 0) {
686 scopeDeriver.deriveScope(scopeCtx);
687 }
688 return scopeCtx.derivedScope;
689 }
690
691 private void scopes(int parent, Dependency child) {
692 scopeCtx.parentScope = (parent > 0) ? parentScopes.get(parent - 1) : null;
693 scopeCtx.derivedScope = scope(child);
694 scopeCtx.childScope = scope(child);
695 }
696
697 private String scope(Dependency dependency) {
698 return (dependency != null) ? dependency.getScope() : null;
699 }
700
701 private boolean deriveOptional(DependencyNode node, String conflictId) {
702 Dependency dep = node.getDependency();
703 boolean optional = (dep != null) && dep.isOptional();
704 if (optional
705 || (node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0
706 || (conflictId != null && resolvedIds.containsKey(conflictId))) {
707 return optional;
708 }
709 int depth = parentNodes.size();
710 return (depth > 0) ? parentOptionals.get(depth - 1) : false;
711 }
712 }
713
714 /**
715 * A context used to hold information that is relevant for deriving the scope of a child dependency.
716 *
717 * @see ConflictResolver.ScopeDeriver
718 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
719 * change without notice and only exists to enable unit testing
720 */
721 private static final class ScopeContext extends ConflictResolver.ScopeContext {
722 private String parentScope;
723 private String childScope;
724 private String derivedScope;
725
726 /**
727 * Creates a new scope context with the specified properties.
728 *
729 * @param parentScope the scope of the parent dependency, may be {@code null}
730 * @param childScope the scope of the child dependency, may be {@code null}
731 * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
732 * change without notice and only exists to enable unit testing
733 */
734 private ScopeContext(String parentScope, String childScope) {
735 this.parentScope = (parentScope != null) ? parentScope : "";
736 derivedScope = (childScope != null) ? childScope : "";
737 this.childScope = (childScope != null) ? childScope : "";
738 }
739
740 /**
741 * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
742 * the scope deriver.
743 *
744 * @return the scope of the parent dependency, never {@code null}
745 */
746 @Override
747 public String getParentScope() {
748 return parentScope;
749 }
750
751 /**
752 * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
753 * descriptor of the parent dependency.
754 *
755 * @return the original scope of the child dependency, never {@code null}
756 */
757 @Override
758 public String getChildScope() {
759 return childScope;
760 }
761
762 /**
763 * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
764 * scope deriver makes changes.
765 *
766 * @return the derived scope of the child dependency, never {@code null}
767 */
768 @Override
769 public String getDerivedScope() {
770 return derivedScope;
771 }
772
773 /**
774 * Sets the derived scope of the child dependency.
775 *
776 * @param derivedScope the derived scope of the dependency, may be {@code null}
777 */
778 @Override
779 public void setDerivedScope(String derivedScope) {
780 this.derivedScope = (derivedScope != null) ? derivedScope : "";
781 }
782 }
783
784 /**
785 * A conflicting dependency.
786 *
787 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
788 * change without notice and only exists to enable unit testing
789 */
790 private static final class ConflictItem extends ConflictResolver.ConflictItem {
791
792 // nodes can share child lists, we care about the unique owner of a child node which is the child list
793 final List<DependencyNode> parent;
794
795 // only for debugging/toString() to help identify the parent node(s)
796 final Artifact artifact;
797
798 // is mutable as removeLosers will mutate it (if Verbosity==STANDARD)
799 DependencyNode node;
800
801 int depth;
802
803 // we start with String and update to Set<String> if needed
804 Object scopes;
805
806 // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE
807 int optionalities;
808
809 private ConflictItem(DependencyNode parent, DependencyNode node, String scope, boolean optional) {
810 if (parent != null) {
811 this.parent = parent.getChildren();
812 this.artifact = parent.getArtifact();
813 } else {
814 this.parent = null;
815 this.artifact = null;
816 }
817 this.node = node;
818 this.scopes = scope;
819 this.optionalities = optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
820 }
821
822 /**
823 * Determines whether the specified conflict item is a sibling of this item.
824 *
825 * @param item the other conflict item, must not be {@code null}
826 * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise
827 */
828 @Override
829 public boolean isSibling(ConflictResolver.ConflictItem item) {
830 return parent == ((ConflictItem) item).parent;
831 }
832
833 /**
834 * Gets the dependency node involved in the conflict.
835 *
836 * @return the involved dependency node, never {@code null}
837 */
838 @Override
839 public DependencyNode getNode() {
840 return node;
841 }
842
843 /**
844 * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
845 *
846 * @return the involved dependency, never {@code null}
847 */
848 @Override
849 public Dependency getDependency() {
850 return node.getDependency();
851 }
852
853 /**
854 * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
855 * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
856 * possible depth.
857 *
858 * @return the zero-based depth of the node in the graph
859 */
860 @Override
861 public int getDepth() {
862 return depth;
863 }
864
865 /**
866 * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
867 * different paths and each path might result in a different derived scope.
868 *
869 * @return the (read-only) set of derived scopes of the dependency, never {@code null}
870 * @see ConflictResolver.ScopeDeriver
871 */
872 @SuppressWarnings("unchecked")
873 @Override
874 public Collection<String> getScopes() {
875 if (scopes instanceof String) {
876 return Collections.singleton((String) scopes);
877 }
878 return (Collection<String>) scopes;
879 }
880
881 @SuppressWarnings("unchecked")
882 void addScope(String scope) {
883 if (scopes instanceof Collection) {
884 ((Collection<String>) scopes).add(scope);
885 } else if (!scopes.equals(scope)) {
886 Collection<String> set = new HashSet<>();
887 set.add((String) scopes);
888 set.add(scope);
889 scopes = set;
890 }
891 }
892
893 /**
894 * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
895 * different paths and each path might result in a different derived optionality.
896 *
897 * @return a bit field consisting of {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
898 * {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
899 * dependency was encountered with
900 */
901 @Override
902 public int getOptionalities() {
903 return optionalities;
904 }
905
906 void addOptional(boolean optional) {
907 optionalities |= optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
908 }
909
910 @Override
911 public String toString() {
912 return node + " @ " + depth + " < " + artifact;
913 }
914 }
915
916 /**
917 * A context used to hold information that is relevant for resolving version and scope conflicts.
918 *
919 * @see ConflictResolver.VersionSelector
920 * @see ConflictResolver.ScopeSelector
921 * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
922 * change without notice and only exists to enable unit testing
923 */
924 private static final class ConflictContext extends ConflictResolver.ConflictContext {
925 final DependencyNode root;
926 final Map<DependencyNode, String> conflictIds;
927 final Collection<ConflictResolver.ConflictItem> items;
928
929 String conflictId;
930 ConflictResolver.ConflictItem winner;
931 String scope;
932 Boolean optional;
933
934 private ConflictContext(
935 DependencyNode root, Map<DependencyNode, String> conflictIds, Collection<ConflictItem> items) {
936 this.root = root;
937 this.conflictIds = conflictIds;
938 this.items = Collections.unmodifiableCollection(items);
939 }
940
941 /**
942 * Gets the root node of the dependency graph being transformed.
943 *
944 * @return the root node of the dependency graph, never {@code null}
945 */
946 @Override
947 public DependencyNode getRoot() {
948 return root;
949 }
950
951 /**
952 * Determines whether the specified dependency node belongs to this conflict context.
953 *
954 * @param node the dependency node to check, must not be {@code null}
955 * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise
956 */
957 @Override
958 public boolean isIncluded(DependencyNode node) {
959 return conflictId.equals(conflictIds.get(node));
960 }
961
962 /**
963 * Gets the collection of conflict items in this context.
964 *
965 * @return the (read-only) collection of conflict items in this context, never {@code null}
966 */
967 @Override
968 public Collection<ConflictResolver.ConflictItem> getItems() {
969 return items;
970 }
971
972 /**
973 * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
974 *
975 * @return the winning conflict item or {@code null} if not set yet
976 */
977 @Override
978 public ConflictResolver.ConflictItem getWinner() {
979 return winner;
980 }
981
982 /**
983 * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
984 *
985 * @param winner the winning conflict item, may be {@code null}
986 */
987 @Override
988 public void setWinner(ConflictResolver.ConflictItem winner) {
989 this.winner = winner;
990 }
991
992 /**
993 * Gets the effective scope of the winning dependency.
994 *
995 * @return the effective scope of the winning dependency or {@code null} if none
996 */
997 @Override
998 public String getScope() {
999 return scope;
1000 }
1001
1002 /**
1003 * Sets the effective scope of the winning dependency.
1004 *
1005 * @param scope the effective scope, may be {@code null}
1006 */
1007 @Override
1008 public void setScope(String scope) {
1009 this.scope = scope;
1010 }
1011
1012 /**
1013 * Gets the effective optional flag of the winning dependency.
1014 *
1015 * @return the effective optional flag or {@code null} if none
1016 */
1017 @Override
1018 public Boolean getOptional() {
1019 return optional;
1020 }
1021
1022 /**
1023 * Sets the effective optional flag of the winning dependency.
1024 *
1025 * @param optional the effective optional flag, may be {@code null}
1026 */
1027 @Override
1028 public void setOptional(Boolean optional) {
1029 this.optional = optional;
1030 }
1031
1032 @Override
1033 public String toString() {
1034 return winner + " @ " + scope + " < " + items;
1035 }
1036 }
1037 }